Repository: ippontech/tatami Branch: master Commit: 90fdbfc474dc Files: 1921 Total size: 12.9 MB Directory structure: gitextract_2urwtjut/ ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── etc/ │ └── installation/ │ └── ubuntu/ │ ├── files/ │ │ └── maven/ │ │ └── settings.xml │ ├── install.sh │ ├── uninstall.sh │ └── update.sh ├── jenkinsScripts/ │ ├── insertGoogleAuthKeys.sh │ ├── restoreDatabase.sh │ ├── saveDatabase.sh │ ├── startTatami.sh │ └── stopTatami.sh ├── mobile/ │ ├── .bowerrc │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── bower.json │ ├── config.xml │ ├── gulpfile.js │ ├── hooks/ │ │ ├── README.md │ │ └── after_prepare/ │ │ └── 010_add_platform_class.js │ ├── ionic.project │ ├── karma.ci.conf.js │ ├── package.json │ ├── pom.xml │ ├── resources/ │ │ ├── icon.psd │ │ └── splash.psd │ ├── scss/ │ │ └── ionic.app.scss │ └── www/ │ ├── app/ │ │ ├── components/ │ │ │ ├── follow/ │ │ │ │ ├── follow.html │ │ │ │ ├── follow.js │ │ │ │ ├── follower/ │ │ │ │ │ ├── follower.controller.js │ │ │ │ │ ├── follower.html │ │ │ │ │ └── follower.js │ │ │ │ ├── following/ │ │ │ │ │ ├── following.controller.js │ │ │ │ │ ├── following.html │ │ │ │ │ └── following.js │ │ │ │ └── suggested/ │ │ │ │ ├── suggested.controller.js │ │ │ │ ├── suggested.html │ │ │ │ └── suggested.js │ │ │ ├── home/ │ │ │ │ ├── favorites/ │ │ │ │ │ ├── favorites.controller.js │ │ │ │ │ ├── favorites.html │ │ │ │ │ └── favorites.js │ │ │ │ ├── home.html │ │ │ │ ├── home.js │ │ │ │ ├── mentions/ │ │ │ │ │ ├── mentions.controller.js │ │ │ │ │ ├── mentions.html │ │ │ │ │ └── mentions.js │ │ │ │ ├── more/ │ │ │ │ │ ├── all_users/ │ │ │ │ │ │ ├── all.users.controller.js │ │ │ │ │ │ ├── all.users.html │ │ │ │ │ │ └── all.users.js │ │ │ │ │ ├── blocked_users/ │ │ │ │ │ │ ├── blocked.users.controller.js │ │ │ │ │ │ ├── blocked.users.html │ │ │ │ │ │ └── blocked.users.js │ │ │ │ │ ├── company/ │ │ │ │ │ │ ├── company-timeline.html │ │ │ │ │ │ ├── company.timeline.controller.js │ │ │ │ │ │ └── company.timeline.js │ │ │ │ │ ├── more.controller.js │ │ │ │ │ ├── more.html │ │ │ │ │ ├── more.js │ │ │ │ │ ├── reportedStatus/ │ │ │ │ │ │ ├── reportedStatus.controller.js │ │ │ │ │ │ ├── reportedStatus.html │ │ │ │ │ │ └── reportedStatus.js │ │ │ │ │ └── settings/ │ │ │ │ │ ├── settings.controller.js │ │ │ │ │ ├── settings.html │ │ │ │ │ └── settings.js │ │ │ │ └── timeline/ │ │ │ │ ├── timeline.controller.js │ │ │ │ ├── timeline.html │ │ │ │ └── timeline.js │ │ │ ├── login/ │ │ │ │ ├── login.controller.js │ │ │ │ ├── login.html │ │ │ │ ├── login.js │ │ │ │ └── server/ │ │ │ │ ├── server.controller.js │ │ │ │ ├── server.html │ │ │ │ └── server.js │ │ │ └── post/ │ │ │ ├── post.controller.js │ │ │ ├── post.html │ │ │ ├── post.js │ │ │ └── postbar.directive.js │ │ ├── shared/ │ │ │ ├── config/ │ │ │ │ ├── marked.config.js │ │ │ │ ├── marked.filter.js │ │ │ │ └── tatami.marked.js │ │ │ ├── interceptor/ │ │ │ │ └── auth.interceptor.js │ │ │ ├── providers/ │ │ │ │ ├── provider.js │ │ │ │ └── tatami.state.provider.js │ │ │ ├── services/ │ │ │ │ ├── HomeService.js │ │ │ │ ├── ProfileService.js │ │ │ │ ├── StatusService.js │ │ │ │ ├── UserService.js │ │ │ │ ├── account.service.js │ │ │ │ ├── block.service.js │ │ │ │ ├── localStorage.service.js │ │ │ │ ├── path.service.js │ │ │ │ ├── report.service.js │ │ │ │ ├── service.js │ │ │ │ ├── tag.service.js │ │ │ │ └── toast.service.js │ │ │ ├── state/ │ │ │ │ ├── conversation/ │ │ │ │ │ ├── conversation.controller.js │ │ │ │ │ └── conversation.html │ │ │ │ ├── profile/ │ │ │ │ │ ├── profile.controller.js │ │ │ │ │ ├── profile.html │ │ │ │ │ └── userOptionsMenu.html │ │ │ │ └── tag/ │ │ │ │ ├── tag.controller.js │ │ │ │ └── tag.html │ │ │ ├── status/ │ │ │ │ ├── blockUserMenu.html │ │ │ │ ├── list/ │ │ │ │ │ ├── status-list.html │ │ │ │ │ └── status.list.directive.js │ │ │ │ ├── status.directive.js │ │ │ │ ├── status.html │ │ │ │ └── status.refresher.service.js │ │ │ └── user/ │ │ │ ├── user-detail.html │ │ │ ├── user.detail.directive.js │ │ │ ├── user.directive.js │ │ │ ├── user.html │ │ │ ├── user.refresher.service.js │ │ │ ├── users.directive.js │ │ │ └── users.html │ │ ├── tatami.controller.js │ │ ├── tatami.endpoint.js │ │ ├── tatami.html │ │ └── tatamiApp.js │ ├── css/ │ │ ├── ionic.app.css │ │ └── style.css │ ├── i18n/ │ │ ├── en/ │ │ │ ├── conversation.json │ │ │ ├── follow.json │ │ │ ├── home.json │ │ │ ├── login.json │ │ │ ├── more.json │ │ │ ├── post.json │ │ │ ├── server.json │ │ │ ├── status.json │ │ │ └── user.json │ │ └── fr/ │ │ ├── conversation.json │ │ ├── follow.json │ │ ├── home.json │ │ ├── login.json │ │ ├── more.json │ │ ├── post.json │ │ ├── server.json │ │ ├── status.json │ │ └── user.json │ ├── index.html │ └── test/ │ └── javascript/ │ └── components/ │ └── profile/ │ └── profile.controller.spec.js ├── pom.xml ├── scripts/ │ └── insertBuildVersion.sh ├── services/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ ├── fr/ │ │ │ │ └── ippon/ │ │ │ │ └── tatami/ │ │ │ │ ├── config/ │ │ │ │ │ ├── ApplicationConfiguration.java │ │ │ │ │ ├── AsyncConfiguration.java │ │ │ │ │ ├── CacheConfiguration.java │ │ │ │ │ ├── CassandraConfiguration.java │ │ │ │ │ ├── ColumnFamilyKeys.java │ │ │ │ │ ├── Constants.java │ │ │ │ │ ├── DispatcherServletConfig.java │ │ │ │ │ ├── GroupRoles.java │ │ │ │ │ ├── MailConfiguration.java │ │ │ │ │ ├── MetricsConfiguration.java │ │ │ │ │ ├── SearchConfiguration.java │ │ │ │ │ └── metrics/ │ │ │ │ │ ├── CassandraHealthCheck.java │ │ │ │ │ └── JavaMailHealthCheck.java │ │ │ │ ├── domain/ │ │ │ │ │ ├── Attachment.java │ │ │ │ │ ├── Avatar.java │ │ │ │ │ ├── DigestType.java │ │ │ │ │ ├── Domain.java │ │ │ │ │ ├── DomainConfiguration.java │ │ │ │ │ ├── Group.java │ │ │ │ │ ├── User.java │ │ │ │ │ ├── UserStatusStat.java │ │ │ │ │ ├── status/ │ │ │ │ │ │ ├── AbstractStatus.java │ │ │ │ │ │ ├── Announcement.java │ │ │ │ │ │ ├── MentionFriend.java │ │ │ │ │ │ ├── MentionShare.java │ │ │ │ │ │ ├── Share.java │ │ │ │ │ │ ├── Status.java │ │ │ │ │ │ ├── StatusDetails.java │ │ │ │ │ │ └── StatusType.java │ │ │ │ │ └── validation/ │ │ │ │ │ ├── ContraintsAttachmentCreation.java │ │ │ │ │ └── ContraintsUserCreation.java │ │ │ │ ├── repository/ │ │ │ │ │ ├── AppleDeviceRepository.java │ │ │ │ │ ├── AppleDeviceUserRepository.java │ │ │ │ │ ├── AttachmentRepository.java │ │ │ │ │ ├── AvatarRepository.java │ │ │ │ │ ├── BlockRepository.java │ │ │ │ │ ├── CounterRepository.java │ │ │ │ │ ├── DaylineRepository.java │ │ │ │ │ ├── DiscussionRepository.java │ │ │ │ │ ├── DomainConfigurationRepository.java │ │ │ │ │ ├── DomainRepository.java │ │ │ │ │ ├── DomainlineRepository.java │ │ │ │ │ ├── FavoritelineRepository.java │ │ │ │ │ ├── FollowerRepository.java │ │ │ │ │ ├── FriendRepository.java │ │ │ │ │ ├── GroupCounterRepository.java │ │ │ │ │ ├── GroupDetailsRepository.java │ │ │ │ │ ├── GroupMembersRepository.java │ │ │ │ │ ├── GroupRepository.java │ │ │ │ │ ├── GrouplineRepository.java │ │ │ │ │ ├── IdempotentRepository.java │ │ │ │ │ ├── MailDigestRepository.java │ │ │ │ │ ├── MentionlineRepository.java │ │ │ │ │ ├── RegistrationRepository.java │ │ │ │ │ ├── ResolvedReportRepository.java │ │ │ │ │ ├── RssUidRepository.java │ │ │ │ │ ├── SharesRepository.java │ │ │ │ │ ├── StatusAttachmentRepository.java │ │ │ │ │ ├── StatusReportRepository.java │ │ │ │ │ ├── StatusRepository.java │ │ │ │ │ ├── TagCounterRepository.java │ │ │ │ │ ├── TagFollowerRepository.java │ │ │ │ │ ├── TaglineRepository.java │ │ │ │ │ ├── TimelineRepository.java │ │ │ │ │ ├── TrendRepository.java │ │ │ │ │ ├── UserAttachmentRepository.java │ │ │ │ │ ├── UserGroupRepository.java │ │ │ │ │ ├── UserRepository.java │ │ │ │ │ ├── UserTagRepository.java │ │ │ │ │ ├── UserTrendRepository.java │ │ │ │ │ ├── UserlineRepository.java │ │ │ │ │ └── cassandra/ │ │ │ │ │ ├── AbstractCassandraFollowerRepository.java │ │ │ │ │ ├── AbstractCassandraFriendRepository.java │ │ │ │ │ ├── AbstractCassandraLineRepository.java │ │ │ │ │ ├── CassandraAppleDeviceRepository.java │ │ │ │ │ ├── CassandraAppleDeviceUserRepository.java │ │ │ │ │ ├── CassandraAttachmentRepository.java │ │ │ │ │ ├── CassandraAvatarRepository.java │ │ │ │ │ ├── CassandraBlockRepository.java │ │ │ │ │ ├── CassandraCounterRepository.java │ │ │ │ │ ├── CassandraDaylineRepository.java │ │ │ │ │ ├── CassandraDiscussionRepository.java │ │ │ │ │ ├── CassandraDomainConfigurationRepository.java │ │ │ │ │ ├── CassandraDomainRepository.java │ │ │ │ │ ├── CassandraDomainlineRepository.java │ │ │ │ │ ├── CassandraFavoritelineRepository.java │ │ │ │ │ ├── CassandraFollowerRepository.java │ │ │ │ │ ├── CassandraFriendRepository.java │ │ │ │ │ ├── CassandraGroupCounterRepository.java │ │ │ │ │ ├── CassandraGroupDetailsRepository.java │ │ │ │ │ ├── CassandraGroupMembersRepository.java │ │ │ │ │ ├── CassandraGroupRepository.java │ │ │ │ │ ├── CassandraGrouplineRepository.java │ │ │ │ │ ├── CassandraIdempotentRepository.java │ │ │ │ │ ├── CassandraMailDigestRepository.java │ │ │ │ │ ├── CassandraMentionlineRepository.java │ │ │ │ │ ├── CassandraRegistrationRepository.java │ │ │ │ │ ├── CassandraRssUidRepository.java │ │ │ │ │ ├── CassandraSharesRepository.java │ │ │ │ │ ├── CassandraStatusAttachmentRepository.java │ │ │ │ │ ├── CassandraStatusReportRepository.java │ │ │ │ │ ├── CassandraStatusRepository.java │ │ │ │ │ ├── CassandraTagCounterRepository.java │ │ │ │ │ ├── CassandraTagFollowerRepository.java │ │ │ │ │ ├── CassandraTaglineRepository.java │ │ │ │ │ ├── CassandraTimelineRepository.java │ │ │ │ │ ├── CassandraTrendRepository.java │ │ │ │ │ ├── CassandraUserAttachmentRepository.java │ │ │ │ │ ├── CassandraUserGroupRepository.java │ │ │ │ │ ├── CassandraUserRepository.java │ │ │ │ │ ├── CassandraUserTagRepository.java │ │ │ │ │ ├── CassandraUserTrendRepository.java │ │ │ │ │ └── CassandraUserlineRepository.java │ │ │ │ ├── security/ │ │ │ │ │ ├── AjaxAuthenticationFailureHandler.java │ │ │ │ │ ├── AjaxAuthenticationSuccessHandler.java │ │ │ │ │ ├── AjaxLogoutSuccessHandler.java │ │ │ │ │ ├── AuthenticationService.java │ │ │ │ │ ├── DomainViolationException.java │ │ │ │ │ ├── GoogleApiAuthenticationProvider.java │ │ │ │ │ ├── GoogleAuthenticationProvider.java │ │ │ │ │ ├── GoogleAuthenticationToken.java │ │ │ │ │ ├── GoogleAutoRegisteringUserDetailsService.java │ │ │ │ │ ├── Http401UnauthorizedEntryPoint.java │ │ │ │ │ ├── OpenIdAutoRegisteringUserDetailsService.java │ │ │ │ │ ├── TatamiAuthenticationSuccessHandler.java │ │ │ │ │ ├── TatamiLdapAuthenticationProvider.java │ │ │ │ │ ├── TatamiUserDetailsService.java │ │ │ │ │ └── xauth/ │ │ │ │ │ ├── Token.java │ │ │ │ │ ├── TokenProvider.java │ │ │ │ │ └── XAuthTokenFilter.java │ │ │ │ ├── service/ │ │ │ │ │ ├── AdminService.java │ │ │ │ │ ├── ApplePushService.java │ │ │ │ │ ├── AtmosphereService.java │ │ │ │ │ ├── AttachmentService.java │ │ │ │ │ ├── AvatarService.java │ │ │ │ │ ├── BlockService.java │ │ │ │ │ ├── CounterService.java │ │ │ │ │ ├── FriendshipService.java │ │ │ │ │ ├── GroupService.java │ │ │ │ │ ├── MailDigestService.java │ │ │ │ │ ├── MailService.java │ │ │ │ │ ├── MentionService.java │ │ │ │ │ ├── SearchService.java │ │ │ │ │ ├── StatsService.java │ │ │ │ │ ├── StatusUpdateService.java │ │ │ │ │ ├── SuggestionService.java │ │ │ │ │ ├── TagMembershipService.java │ │ │ │ │ ├── TimelineService.java │ │ │ │ │ ├── TrendService.java │ │ │ │ │ ├── UserService.java │ │ │ │ │ ├── dto/ │ │ │ │ │ │ ├── StatusDTO.java │ │ │ │ │ │ ├── UserDTO.java │ │ │ │ │ │ └── UserGroupDTO.java │ │ │ │ │ ├── elasticsearch/ │ │ │ │ │ │ ├── ElasticsearchEngine.java │ │ │ │ │ │ ├── ElasticsearchSearchService.java │ │ │ │ │ │ ├── EmbeddedElasticsearchEngine.java │ │ │ │ │ │ └── RemoteElasticsearchEngine.java │ │ │ │ │ ├── exception/ │ │ │ │ │ │ ├── ArchivedGroupException.java │ │ │ │ │ │ ├── ReplyStatusException.java │ │ │ │ │ │ └── StorageSizeException.java │ │ │ │ │ └── util/ │ │ │ │ │ ├── AnalysisUtil.java │ │ │ │ │ ├── DomainUtil.java │ │ │ │ │ ├── RandomUtil.java │ │ │ │ │ └── ValueComparator.java │ │ │ │ └── web/ │ │ │ │ ├── atmosphere/ │ │ │ │ │ └── TatamiNotification.java │ │ │ │ ├── rest/ │ │ │ │ │ └── dto/ │ │ │ │ │ ├── ActionStatus.java │ │ │ │ │ ├── EmailAndUsername.java │ │ │ │ │ ├── Preferences.java │ │ │ │ │ ├── Reply.java │ │ │ │ │ ├── SearchResults.java │ │ │ │ │ ├── Tag.java │ │ │ │ │ ├── Trend.java │ │ │ │ │ ├── UserActionStatus.java │ │ │ │ │ └── UserPassword.java │ │ │ │ └── syndic/ │ │ │ │ ├── SyndicTimelineController.java │ │ │ │ ├── SyndicView.java │ │ │ │ └── UnknownRssChannelException.java │ │ │ └── me/ │ │ │ └── prettyprint/ │ │ │ └── hom/ │ │ │ └── CassandraPersistenceProvider.java │ │ ├── resources/ │ │ │ ├── META-INF/ │ │ │ │ ├── elasticsearch/ │ │ │ │ │ ├── elasticsearch-embedded.yml │ │ │ │ │ ├── index/ │ │ │ │ │ │ ├── group.json │ │ │ │ │ │ ├── status.json │ │ │ │ │ │ └── user.json │ │ │ │ │ └── logging.yml │ │ │ │ ├── spring/ │ │ │ │ │ ├── applicationContext-metrics.xml │ │ │ │ │ └── applicationContext-security.xml │ │ │ │ └── tatami/ │ │ │ │ ├── customization.properties │ │ │ │ ├── mails/ │ │ │ │ │ ├── common │ │ │ │ │ ├── dailyDigestEmail │ │ │ │ │ ├── deactivatedUserEmail │ │ │ │ │ ├── invitationMessageEmail │ │ │ │ │ ├── lostPasswordEmail │ │ │ │ │ ├── messages/ │ │ │ │ │ │ ├── messages_en.properties │ │ │ │ │ │ └── messages_fr.properties │ │ │ │ │ ├── passwordReinitializedEmail │ │ │ │ │ ├── registrationEmail │ │ │ │ │ ├── reportedStatusEmail │ │ │ │ │ ├── userMentionEmail │ │ │ │ │ ├── userPrivateMessageEmail │ │ │ │ │ ├── validationEmail │ │ │ │ │ └── weeklyDigestEmail │ │ │ │ └── tatami.properties │ │ │ ├── ehcache.xml │ │ │ └── logback.xml │ │ └── webapp/ │ │ └── app/ │ │ └── shared/ │ │ └── services/ │ │ ├── AuthenticationService.js │ │ ├── GeolocService.js │ │ ├── GroupService.js │ │ ├── HomeService.js │ │ ├── ProfileService.js │ │ ├── SearchService.js │ │ ├── StatusService.js │ │ ├── TagService.js │ │ ├── TopPostersService.js │ │ ├── UserService.js │ │ └── UserSession.js │ └── test/ │ ├── java/ │ │ └── fr/ │ │ └── ippon/ │ │ └── tatami/ │ │ ├── AbstractCassandraTatamiTest.java │ │ ├── repository/ │ │ │ ├── MailDigestRepositoryTest.java │ │ │ ├── StatusReportRepositoryTest.java │ │ │ ├── StatusRepositoryTest.java │ │ │ ├── TagFollowerRepositoryTest.java │ │ │ └── UserRepositoryTest.java │ │ ├── service/ │ │ │ ├── FriendshipServiceTest.java │ │ │ ├── GroupServiceTest.java │ │ │ ├── MailDigestServiceTest.java │ │ │ ├── StatsServiceTest.java │ │ │ ├── StatusDeletionTest.java │ │ │ ├── StatusUpdateServiceTest.java │ │ │ ├── TagMembershipServiceTest.java │ │ │ ├── TimelineServiceTest.java │ │ │ ├── TrendServiceTest.java │ │ │ ├── UserServiceTest.java │ │ │ └── elasticsearch/ │ │ │ └── ElasticsearchSearchServiceTest.java │ │ ├── test/ │ │ │ ├── MockUtils.java │ │ │ └── application/ │ │ │ ├── ApplicationTestConfiguration.java │ │ │ └── WebApplicationTestConfiguration.java │ │ └── web/ │ │ └── syndic/ │ │ └── SyndicTimelineControllerTest.java │ ├── jmeter/ │ │ ├── tatami-create-users.jmx │ │ └── tatami-stress-test.jmx │ └── resources/ │ ├── dataset/ │ │ └── dataset.json │ ├── logback.xml │ └── tatami/ │ └── tatami-test.properties ├── src/ │ ├── integration/ │ │ ├── java/ │ │ │ ├── fr/ │ │ │ │ └── ippon/ │ │ │ │ └── tatami/ │ │ │ │ ├── test/ │ │ │ │ │ └── support/ │ │ │ │ │ ├── LdapTestServer.java │ │ │ │ │ └── LdapTestServerJunitLauncher.java │ │ │ │ └── uitest/ │ │ │ │ ├── AuthenticationSpec.groovy │ │ │ │ ├── NewRegistrationSpec.groovy │ │ │ │ └── support/ │ │ │ │ ├── AccountUtils.groovy │ │ │ │ ├── CassandraAccessUtils.groovy │ │ │ │ ├── RegistrationUtils.groovy │ │ │ │ └── TatamiBaseGebSpec.groovy │ │ │ └── pages/ │ │ │ ├── EmailVerifiedPage.groovy │ │ │ ├── HomePage.groovy │ │ │ ├── LoginPage.groovy │ │ │ ├── TatamiBasePage.groovy │ │ │ └── google/ │ │ │ ├── GoogleAuthenticationPage.groovy │ │ │ └── GoogleOpenIdPage.groovy │ │ └── resources/ │ │ ├── GebConfig.groovy │ │ └── fr/ │ │ └── ippon/ │ │ └── tatami/ │ │ └── test/ │ │ └── support/ │ │ └── ipponTestLdapExport.ldif │ └── main/ │ └── cql/ │ ├── install.cql │ └── upgrade/ │ ├── upgrade_from_1.0.27_to_2.0.0.cql │ ├── upgrade_from_2.0.0_to_2.1.0.cql │ ├── upgrade_from_2.1.3_to_2.2.0.cql │ └── upgrade_from_3.0.26_to_3.0.27.cql ├── tatamibot/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── fr/ │ │ │ └── ippon/ │ │ │ └── tatami/ │ │ │ ├── bot/ │ │ │ │ ├── Tatamibot.java │ │ │ │ ├── config/ │ │ │ │ │ └── TatamibotConfiguration.java │ │ │ │ ├── processor/ │ │ │ │ │ ├── LastUpdateDateTatamibotConfigurationUpdater.java │ │ │ │ │ └── TatamiStatusProcessor.java │ │ │ │ └── route/ │ │ │ │ ├── CommonRouteBuilder.java │ │ │ │ ├── GitHubRouteBuilder.java │ │ │ │ ├── RssRouteBuilder.java │ │ │ │ ├── SourceRouteBuilderBase.java │ │ │ │ └── TwitterRouteBuilder.java │ │ │ ├── repository/ │ │ │ │ ├── TatamibotConfigurationRepository.java │ │ │ │ └── cassandra/ │ │ │ │ └── CassandraTatamibotConfigurationRepository.java │ │ │ └── web/ │ │ │ └── bot/ │ │ │ └── TatamibotController.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── spring/ │ │ └── applicationContext-tatamibot.xml │ └── test/ │ ├── java/ │ │ └── fr/ │ │ └── ippon/ │ │ └── tatami/ │ │ ├── bot/ │ │ │ ├── TatamibotTest.java │ │ │ └── route/ │ │ │ ├── CommonRouteBuilderTest.java │ │ │ ├── RssRouteBuilderCamelTest.java │ │ │ ├── RssRouteBuilderUnitTest.java │ │ │ ├── SourceRouteBuilderBaseCamelTest.java │ │ │ └── TwitterRouteBuilderCamelTest.java │ │ └── test/ │ │ └── MockUtils.java │ └── resources/ │ └── fr/ │ └── ippon/ │ └── tatami/ │ └── bot/ │ └── route/ │ └── rss.xml └── web/ ├── gruntfile.js ├── karma.ci.conf.js ├── karma.conf.js ├── package.json ├── pom.xml └── src/ ├── main/ │ ├── java/ │ │ └── fr/ │ │ └── ippon/ │ │ └── tatami/ │ │ └── web/ │ │ ├── atmosphere/ │ │ │ └── RealtimeService.java │ │ ├── controller/ │ │ │ ├── ErrorController.java │ │ │ ├── HomeController.java │ │ │ └── Pac4JSecurityCheckController.java │ │ ├── fileupload/ │ │ │ ├── FileController.java │ │ │ ├── Message.java │ │ │ ├── StatusResponse.java │ │ │ └── UploadedFile.java │ │ ├── filter/ │ │ │ ├── IeRefreshWrapper.java │ │ │ └── TatamiGzipFilter.java │ │ ├── init/ │ │ │ └── WebConfigurer.java │ │ └── rest/ │ │ ├── AccountController.java │ │ ├── AttachmentController.java │ │ ├── BlockController.java │ │ ├── CompanyWallController.java │ │ ├── FavoritesController.java │ │ ├── FriendshipController.java │ │ ├── GroupController.java │ │ ├── MentionsController.java │ │ ├── SearchController.java │ │ ├── StatsController.java │ │ ├── TagController.java │ │ ├── TimelineController.java │ │ ├── TrendController.java │ │ ├── UserController.java │ │ └── UserXAuthController.java │ └── webapp/ │ ├── .bowerrc │ ├── WEB-INF/ │ │ ├── messages/ │ │ │ ├── messages_en.properties │ │ │ └── messages_fr.properties │ │ ├── pages/ │ │ │ ├── errors/ │ │ │ │ ├── 404.jsp │ │ │ │ ├── 500.jsp │ │ │ │ └── file_not_found.jsp │ │ │ ├── home.jsp │ │ │ ├── includes/ │ │ │ │ ├── footer.jsp │ │ │ │ ├── header.jsp │ │ │ │ ├── help-home.jsp │ │ │ │ ├── navigation-admin.jsp │ │ │ │ ├── template-search-engine.jsp │ │ │ │ ├── templates-admin.jsp │ │ │ │ ├── templates.jsp │ │ │ │ ├── topavatar.jsp │ │ │ │ └── topmenu.jsp │ │ │ └── login.jsp │ │ └── web.xml │ ├── app/ │ │ ├── TatamiApp.js │ │ ├── components/ │ │ │ ├── about/ │ │ │ │ ├── AboutModule.js │ │ │ │ ├── AboutView.html │ │ │ │ ├── license/ │ │ │ │ │ ├── LicenseController.js │ │ │ │ │ └── LicenseView.html │ │ │ │ ├── presentation/ │ │ │ │ │ └── PresentationView.html │ │ │ │ └── tos/ │ │ │ │ └── ToSView.html │ │ │ ├── account/ │ │ │ │ ├── AccountController.js │ │ │ │ ├── AccountModule.js │ │ │ │ ├── AccountView.html │ │ │ │ ├── FormController.js │ │ │ │ ├── FormView.html │ │ │ │ ├── files/ │ │ │ │ │ ├── FilesController.js │ │ │ │ │ ├── FilesModule.js │ │ │ │ │ ├── FilesService.js │ │ │ │ │ └── FilesView.html │ │ │ │ ├── groups/ │ │ │ │ │ ├── GroupsController.js │ │ │ │ │ ├── GroupsModule.js │ │ │ │ │ ├── GroupsView.html │ │ │ │ │ ├── creation/ │ │ │ │ │ │ ├── GroupsCreateController.js │ │ │ │ │ │ └── GroupsCreateView.html │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── GroupListController.js │ │ │ │ │ │ └── GroupsListView.html │ │ │ │ │ └── manage/ │ │ │ │ │ ├── GroupsManageController.js │ │ │ │ │ └── GroupsManageView.html │ │ │ │ ├── password/ │ │ │ │ │ ├── PasswordController.js │ │ │ │ │ ├── PasswordModule.js │ │ │ │ │ ├── PasswordService.js │ │ │ │ │ └── PasswordView.html │ │ │ │ ├── preferences/ │ │ │ │ │ ├── PreferencesController.js │ │ │ │ │ ├── PreferencesModule.js │ │ │ │ │ ├── PreferencesService.js │ │ │ │ │ └── PreferencesView.html │ │ │ │ ├── profile/ │ │ │ │ │ ├── ProfileController.js │ │ │ │ │ ├── ProfileModule.js │ │ │ │ │ └── ProfileView.html │ │ │ │ ├── tags/ │ │ │ │ │ ├── TagsController.js │ │ │ │ │ ├── TagsModule.js │ │ │ │ │ └── TagsView.html │ │ │ │ ├── topPosters/ │ │ │ │ │ ├── TopPostersController.js │ │ │ │ │ ├── TopPostersModule.js │ │ │ │ │ └── TopPostersView.html │ │ │ │ └── users/ │ │ │ │ ├── UsersController.js │ │ │ │ ├── UsersModule.js │ │ │ │ ├── UsersView.html │ │ │ │ └── directives/ │ │ │ │ ├── UserAccountController.js │ │ │ │ ├── UserAccountDirective.js │ │ │ │ └── UserAccountView.html │ │ │ ├── admin/ │ │ │ │ ├── AdminController.js │ │ │ │ ├── AdminModule.js │ │ │ │ ├── AdminService.js │ │ │ │ └── AdminView.html │ │ │ ├── home/ │ │ │ │ ├── HomeModule.js │ │ │ │ ├── HomeView.html │ │ │ │ ├── group/ │ │ │ │ │ ├── GroupHeaderController.js │ │ │ │ │ └── GroupHeaderView.html │ │ │ │ ├── profile/ │ │ │ │ │ ├── ProfileHeaderController.js │ │ │ │ │ └── ProfileHeaderView.html │ │ │ │ ├── search/ │ │ │ │ │ ├── SearchHeaderController.js │ │ │ │ │ └── SearchHeaderView.html │ │ │ │ ├── status/ │ │ │ │ │ ├── StatusController.js │ │ │ │ │ └── StatusView.html │ │ │ │ ├── tag/ │ │ │ │ │ ├── TagHeaderController.js │ │ │ │ │ └── TagHeaderView.html │ │ │ │ ├── timeline/ │ │ │ │ │ └── TimelineHeaderView.html │ │ │ │ └── welcome/ │ │ │ │ ├── WelcomeController.js │ │ │ │ └── WelcomeView.html │ │ │ └── login/ │ │ │ ├── LoginController.js │ │ │ ├── LoginModule.js │ │ │ ├── LoginView.html │ │ │ ├── RegistrationService.js │ │ │ ├── email/ │ │ │ │ ├── EmailRegistration.html │ │ │ │ └── EmailRegistrationController.js │ │ │ ├── google/ │ │ │ │ ├── GoogleLoginController.js │ │ │ │ └── GoogleLoginView.html │ │ │ ├── manual/ │ │ │ │ ├── ManualLoginController.js │ │ │ │ └── ManualLoginView.html │ │ │ ├── recoverPassword/ │ │ │ │ ├── RecoverPasswordController.js │ │ │ │ └── RecoverPasswordView.html │ │ │ └── register/ │ │ │ ├── RegisterController.js │ │ │ └── RegisterView.html │ │ └── shared/ │ │ ├── configs/ │ │ │ ├── MarkedConfig.js │ │ │ ├── MomentConfig.js │ │ │ └── TranslateConfig.js │ │ ├── error/ │ │ │ ├── 404View.html │ │ │ └── 500View.html │ │ ├── filters/ │ │ │ ├── EmoticonFilter.js │ │ │ ├── MarkdownFilter.js │ │ │ └── PlaceholderFilter.js │ │ ├── footer/ │ │ │ ├── FooterController.js │ │ │ ├── FooterModule.js │ │ │ └── FooterView.html │ │ ├── lists/ │ │ │ ├── status/ │ │ │ │ ├── withContext/ │ │ │ │ │ ├── StatusListContextController.js │ │ │ │ │ └── StatusListContextView.html │ │ │ │ └── withoutContext/ │ │ │ │ ├── StatusListController.js │ │ │ │ └── StatusListView.html │ │ │ └── user/ │ │ │ ├── UserListController.js │ │ │ └── UserListView.html │ │ ├── services/ │ │ │ ├── AuthenticationService.js │ │ │ ├── GeolocService.js │ │ │ ├── GroupService.js │ │ │ ├── HomeService.js │ │ │ ├── ProfileService.js │ │ │ ├── SearchService.js │ │ │ ├── StatusService.js │ │ │ ├── TagService.js │ │ │ ├── TopPostersService.js │ │ │ ├── UserService.js │ │ │ └── UserSession.js │ │ ├── sidebars/ │ │ │ ├── home/ │ │ │ │ ├── HomeSidebarController.js │ │ │ │ ├── HomeSidebarModule.js │ │ │ │ └── HomeSidebarView.html │ │ │ └── profile/ │ │ │ ├── ProfileSidebarController.js │ │ │ ├── ProfileSidebarModule.js │ │ │ └── ProfileSidebarView.html │ │ └── topMenu/ │ │ ├── SearchView.html │ │ ├── TopMenuController.js │ │ ├── TopMenuModule.js │ │ ├── TopMenuView.html │ │ └── post/ │ │ ├── DropdownTagTemplate.html │ │ ├── DropdownUserTemplate.html │ │ ├── PostController.js │ │ ├── PostModule.js │ │ └── PostView.html │ ├── assets/ │ │ ├── bower_components/ │ │ │ ├── angular/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── angular-csp.css │ │ │ │ ├── angular.min.js.gzip │ │ │ │ ├── bower.json │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── angular-animate/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── angular-bootstrap/ │ │ │ │ ├── .bower.json │ │ │ │ └── bower.json │ │ │ ├── angular-bootstrap-tour/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── app/ │ │ │ │ │ ├── angular-bootstrap-tour.js │ │ │ │ │ ├── tour_config_provider.js │ │ │ │ │ ├── tour_controller.js │ │ │ │ │ ├── tour_directive.js │ │ │ │ │ ├── tour_helpers.js │ │ │ │ │ └── tour_step_directive.js │ │ │ │ ├── bower.json │ │ │ │ ├── demo/ │ │ │ │ │ └── angular-bootstrap-tour.js │ │ │ │ ├── dist/ │ │ │ │ │ └── angular-bootstrap-tour.js │ │ │ │ ├── gruntfile.js │ │ │ │ ├── karma.conf.js │ │ │ │ └── test/ │ │ │ │ └── spec/ │ │ │ │ └── angular-bootstrap-tour.spec.js │ │ │ ├── angular-cookies/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── angular-mocks/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── angular-mocks.js │ │ │ │ ├── bower.json │ │ │ │ ├── ngAnimateMock.js │ │ │ │ ├── ngMock.js │ │ │ │ ├── ngMockE2E.js │ │ │ │ └── package.json │ │ │ ├── angular-moment/ │ │ │ │ ├── .bower.json │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── .jshintrc │ │ │ │ ├── .npmignore │ │ │ │ ├── .travis.yml │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── CONTRIBUTING.md │ │ │ │ ├── Gruntfile.js │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── angular-moment.nuspec │ │ │ │ ├── bower.json │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ └── tests.js │ │ │ ├── angular-resource/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── angular-route/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── angular-route.js │ │ │ │ └── bower.json │ │ │ ├── angular-sanitize/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── angular-touch/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── angular-translate/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ └── bower.json │ │ │ ├── angular-translate-storage-cookie/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ └── bower.json │ │ │ ├── angular-ui-router/ │ │ │ │ ├── .bower.json │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── CONTRIBUTING.md │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── api/ │ │ │ │ │ └── angular-ui-router.d.ts │ │ │ │ ├── bower.json │ │ │ │ ├── release/ │ │ │ │ │ └── angular-ui-router.js │ │ │ │ └── src/ │ │ │ │ ├── common.js │ │ │ │ ├── resolve.js │ │ │ │ ├── state.js │ │ │ │ ├── stateDirectives.js │ │ │ │ ├── stateFilters.js │ │ │ │ ├── templateFactory.js │ │ │ │ ├── urlMatcherFactory.js │ │ │ │ ├── urlRouter.js │ │ │ │ ├── view.js │ │ │ │ ├── viewDirective.js │ │ │ │ └── viewScroll.js │ │ │ ├── bootstrap/ │ │ │ │ ├── .bower.json │ │ │ │ ├── DOCS-LICENSE │ │ │ │ ├── LICENSE │ │ │ │ ├── LICENSE-MIT │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ ├── dist/ │ │ │ │ │ ├── css/ │ │ │ │ │ │ ├── bootstrap-theme.css │ │ │ │ │ │ └── bootstrap.css │ │ │ │ │ └── js/ │ │ │ │ │ └── bootstrap.js │ │ │ │ ├── js/ │ │ │ │ │ ├── affix.js │ │ │ │ │ ├── alert.js │ │ │ │ │ ├── button.js │ │ │ │ │ ├── carousel.js │ │ │ │ │ ├── collapse.js │ │ │ │ │ ├── dropdown.js │ │ │ │ │ ├── modal.js │ │ │ │ │ ├── popover.js │ │ │ │ │ ├── scrollspy.js │ │ │ │ │ ├── tab.js │ │ │ │ │ ├── tooltip.js │ │ │ │ │ └── transition.js │ │ │ │ └── less/ │ │ │ │ ├── alerts.less │ │ │ │ ├── badges.less │ │ │ │ ├── bootstrap.less │ │ │ │ ├── breadcrumbs.less │ │ │ │ ├── button-groups.less │ │ │ │ ├── buttons.less │ │ │ │ ├── carousel.less │ │ │ │ ├── close.less │ │ │ │ ├── code.less │ │ │ │ ├── component-animations.less │ │ │ │ ├── dropdowns.less │ │ │ │ ├── forms.less │ │ │ │ ├── glyphicons.less │ │ │ │ ├── grid.less │ │ │ │ ├── input-groups.less │ │ │ │ ├── jumbotron.less │ │ │ │ ├── labels.less │ │ │ │ ├── list-group.less │ │ │ │ ├── media.less │ │ │ │ ├── mixins.less │ │ │ │ ├── modals.less │ │ │ │ ├── navbar.less │ │ │ │ ├── navs.less │ │ │ │ ├── normalize.less │ │ │ │ ├── pager.less │ │ │ │ ├── pagination.less │ │ │ │ ├── panels.less │ │ │ │ ├── popovers.less │ │ │ │ ├── print.less │ │ │ │ ├── progress-bars.less │ │ │ │ ├── responsive-utilities.less │ │ │ │ ├── scaffolding.less │ │ │ │ ├── tables.less │ │ │ │ ├── theme.less │ │ │ │ ├── thumbnails.less │ │ │ │ ├── tooltip.less │ │ │ │ ├── type.less │ │ │ │ ├── utilities.less │ │ │ │ ├── variables.less │ │ │ │ └── wells.less │ │ │ ├── jquery/ │ │ │ │ ├── .bower.json │ │ │ │ ├── MIT-LICENSE.txt │ │ │ │ ├── bower.json │ │ │ │ ├── dist/ │ │ │ │ │ └── jquery.js │ │ │ │ └── src/ │ │ │ │ ├── ajax/ │ │ │ │ │ ├── jsonp.js │ │ │ │ │ ├── load.js │ │ │ │ │ ├── parseJSON.js │ │ │ │ │ ├── parseXML.js │ │ │ │ │ ├── script.js │ │ │ │ │ ├── var/ │ │ │ │ │ │ ├── nonce.js │ │ │ │ │ │ └── rquery.js │ │ │ │ │ └── xhr.js │ │ │ │ ├── ajax.js │ │ │ │ ├── attributes/ │ │ │ │ │ ├── attr.js │ │ │ │ │ ├── classes.js │ │ │ │ │ ├── prop.js │ │ │ │ │ ├── support.js │ │ │ │ │ └── val.js │ │ │ │ ├── attributes.js │ │ │ │ ├── callbacks.js │ │ │ │ ├── core/ │ │ │ │ │ ├── access.js │ │ │ │ │ ├── init.js │ │ │ │ │ ├── parseHTML.js │ │ │ │ │ ├── ready.js │ │ │ │ │ └── var/ │ │ │ │ │ └── rsingleTag.js │ │ │ │ ├── core.js │ │ │ │ ├── css/ │ │ │ │ │ ├── addGetHookIf.js │ │ │ │ │ ├── curCSS.js │ │ │ │ │ ├── defaultDisplay.js │ │ │ │ │ ├── hiddenVisibleSelectors.js │ │ │ │ │ ├── support.js │ │ │ │ │ ├── swap.js │ │ │ │ │ └── var/ │ │ │ │ │ ├── cssExpand.js │ │ │ │ │ ├── isHidden.js │ │ │ │ │ ├── rmargin.js │ │ │ │ │ └── rnumnonpx.js │ │ │ │ ├── css.js │ │ │ │ ├── data.js │ │ │ │ ├── deferred.js │ │ │ │ ├── deprecated.js │ │ │ │ ├── dimensions.js │ │ │ │ ├── effects/ │ │ │ │ │ ├── Tween.js │ │ │ │ │ ├── animatedSelector.js │ │ │ │ │ └── support.js │ │ │ │ ├── effects.js │ │ │ │ ├── event/ │ │ │ │ │ ├── alias.js │ │ │ │ │ └── support.js │ │ │ │ ├── event.js │ │ │ │ ├── exports/ │ │ │ │ │ ├── amd.js │ │ │ │ │ └── global.js │ │ │ │ ├── intro.js │ │ │ │ ├── jquery.js │ │ │ │ ├── manipulation/ │ │ │ │ │ ├── _evalUrl.js │ │ │ │ │ ├── support.js │ │ │ │ │ └── var/ │ │ │ │ │ └── rcheckableType.js │ │ │ │ ├── manipulation.js │ │ │ │ ├── offset.js │ │ │ │ ├── outro.js │ │ │ │ ├── queue/ │ │ │ │ │ └── delay.js │ │ │ │ ├── queue.js │ │ │ │ ├── selector-sizzle.js │ │ │ │ ├── selector.js │ │ │ │ ├── serialize.js │ │ │ │ ├── sizzle/ │ │ │ │ │ └── dist/ │ │ │ │ │ └── sizzle.js │ │ │ │ ├── support.js │ │ │ │ ├── traversing/ │ │ │ │ │ ├── findFilter.js │ │ │ │ │ └── var/ │ │ │ │ │ └── rneedsContext.js │ │ │ │ ├── traversing.js │ │ │ │ ├── var/ │ │ │ │ │ ├── class2type.js │ │ │ │ │ ├── concat.js │ │ │ │ │ ├── deletedIds.js │ │ │ │ │ ├── hasOwn.js │ │ │ │ │ ├── indexOf.js │ │ │ │ │ ├── pnum.js │ │ │ │ │ ├── push.js │ │ │ │ │ ├── rnotwhite.js │ │ │ │ │ ├── slice.js │ │ │ │ │ ├── strundefined.js │ │ │ │ │ ├── support.js │ │ │ │ │ └── toString.js │ │ │ │ └── wrap.js │ │ │ ├── ment.io/ │ │ │ │ ├── .bower.json │ │ │ │ ├── LICENSE-MIT │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ ├── dist/ │ │ │ │ │ ├── mentio.js │ │ │ │ │ └── templates.js │ │ │ │ ├── gulpfile.js │ │ │ │ ├── karma.conf.js │ │ │ │ ├── ment.io/ │ │ │ │ │ ├── index.html │ │ │ │ │ ├── peopledata.json │ │ │ │ │ ├── productdata.json │ │ │ │ │ ├── scripts.js │ │ │ │ │ ├── simplepeopledata.json │ │ │ │ │ └── styles.css │ │ │ │ ├── package.json │ │ │ │ └── src/ │ │ │ │ ├── mentio-menu.tpl.html │ │ │ │ ├── mentio.directive.js │ │ │ │ └── mentio.service.js │ │ │ ├── moment/ │ │ │ │ ├── .bower.json │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── LICENSE │ │ │ │ ├── Moment.js.nuspec │ │ │ │ ├── README.md │ │ │ │ ├── benchmarks/ │ │ │ │ │ └── clone.js │ │ │ │ ├── bower.json │ │ │ │ ├── locale/ │ │ │ │ │ ├── af.js │ │ │ │ │ ├── ar-ma.js │ │ │ │ │ ├── ar-sa.js │ │ │ │ │ ├── ar-tn.js │ │ │ │ │ ├── ar.js │ │ │ │ │ ├── az.js │ │ │ │ │ ├── be.js │ │ │ │ │ ├── bg.js │ │ │ │ │ ├── bn.js │ │ │ │ │ ├── bo.js │ │ │ │ │ ├── br.js │ │ │ │ │ ├── bs.js │ │ │ │ │ ├── ca.js │ │ │ │ │ ├── cs.js │ │ │ │ │ ├── cv.js │ │ │ │ │ ├── cy.js │ │ │ │ │ ├── da.js │ │ │ │ │ ├── de-at.js │ │ │ │ │ ├── de.js │ │ │ │ │ ├── el.js │ │ │ │ │ ├── en-au.js │ │ │ │ │ ├── en-ca.js │ │ │ │ │ ├── en-gb.js │ │ │ │ │ ├── eo.js │ │ │ │ │ ├── es.js │ │ │ │ │ ├── et.js │ │ │ │ │ ├── eu.js │ │ │ │ │ ├── fa.js │ │ │ │ │ ├── fi.js │ │ │ │ │ ├── fo.js │ │ │ │ │ ├── fr-ca.js │ │ │ │ │ ├── fr.js │ │ │ │ │ ├── fy.js │ │ │ │ │ ├── gl.js │ │ │ │ │ ├── he.js │ │ │ │ │ ├── hi.js │ │ │ │ │ ├── hr.js │ │ │ │ │ ├── hu.js │ │ │ │ │ ├── hy-am.js │ │ │ │ │ ├── id.js │ │ │ │ │ ├── is.js │ │ │ │ │ ├── it.js │ │ │ │ │ ├── ja.js │ │ │ │ │ ├── ka.js │ │ │ │ │ ├── km.js │ │ │ │ │ ├── ko.js │ │ │ │ │ ├── lb.js │ │ │ │ │ ├── lt.js │ │ │ │ │ ├── lv.js │ │ │ │ │ ├── mk.js │ │ │ │ │ ├── ml.js │ │ │ │ │ ├── mr.js │ │ │ │ │ ├── ms-my.js │ │ │ │ │ ├── my.js │ │ │ │ │ ├── nb.js │ │ │ │ │ ├── ne.js │ │ │ │ │ ├── nl.js │ │ │ │ │ ├── nn.js │ │ │ │ │ ├── pl.js │ │ │ │ │ ├── pt-br.js │ │ │ │ │ ├── pt.js │ │ │ │ │ ├── ro.js │ │ │ │ │ ├── ru.js │ │ │ │ │ ├── sk.js │ │ │ │ │ ├── sl.js │ │ │ │ │ ├── sq.js │ │ │ │ │ ├── sr-cyrl.js │ │ │ │ │ ├── sr.js │ │ │ │ │ ├── sv.js │ │ │ │ │ ├── ta.js │ │ │ │ │ ├── th.js │ │ │ │ │ ├── tl-ph.js │ │ │ │ │ ├── tr.js │ │ │ │ │ ├── tzm-latn.js │ │ │ │ │ ├── tzm.js │ │ │ │ │ ├── uk.js │ │ │ │ │ ├── uz.js │ │ │ │ │ ├── vi.js │ │ │ │ │ ├── zh-cn.js │ │ │ │ │ └── zh-tw.js │ │ │ │ ├── meteor/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── export.js │ │ │ │ │ └── test.js │ │ │ │ ├── min/ │ │ │ │ │ ├── locales.js │ │ │ │ │ ├── moment-with-locales.js │ │ │ │ │ └── tests.js │ │ │ │ ├── moment.js │ │ │ │ ├── scripts/ │ │ │ │ │ └── npm_prepublish.sh │ │ │ │ ├── src/ │ │ │ │ │ ├── lib/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── check-overflow.js │ │ │ │ │ │ │ ├── date-from-array.js │ │ │ │ │ │ │ ├── default-parsing-flags.js │ │ │ │ │ │ │ ├── from-anything.js │ │ │ │ │ │ │ ├── from-array.js │ │ │ │ │ │ │ ├── from-object.js │ │ │ │ │ │ │ ├── from-string-and-array.js │ │ │ │ │ │ │ ├── from-string-and-format.js │ │ │ │ │ │ │ ├── from-string.js │ │ │ │ │ │ │ ├── local.js │ │ │ │ │ │ │ ├── utc.js │ │ │ │ │ │ │ └── valid.js │ │ │ │ │ │ ├── duration/ │ │ │ │ │ │ │ ├── abs.js │ │ │ │ │ │ │ ├── add-subtract.js │ │ │ │ │ │ │ ├── as.js │ │ │ │ │ │ │ ├── bubble.js │ │ │ │ │ │ │ ├── constructor.js │ │ │ │ │ │ │ ├── create.js │ │ │ │ │ │ │ ├── duration.js │ │ │ │ │ │ │ ├── get.js │ │ │ │ │ │ │ ├── humanize.js │ │ │ │ │ │ │ ├── iso-string.js │ │ │ │ │ │ │ └── prototype.js │ │ │ │ │ │ ├── format/ │ │ │ │ │ │ │ └── format.js │ │ │ │ │ │ ├── locale/ │ │ │ │ │ │ │ ├── calendar.js │ │ │ │ │ │ │ ├── constructor.js │ │ │ │ │ │ │ ├── en.js │ │ │ │ │ │ │ ├── formats.js │ │ │ │ │ │ │ ├── invalid.js │ │ │ │ │ │ │ ├── lists.js │ │ │ │ │ │ │ ├── locale.js │ │ │ │ │ │ │ ├── locales.js │ │ │ │ │ │ │ ├── ordinal.js │ │ │ │ │ │ │ ├── pre-post-format.js │ │ │ │ │ │ │ ├── prototype.js │ │ │ │ │ │ │ ├── relative.js │ │ │ │ │ │ │ └── set.js │ │ │ │ │ │ ├── moment/ │ │ │ │ │ │ │ ├── add-subtract.js │ │ │ │ │ │ │ ├── calendar.js │ │ │ │ │ │ │ ├── clone.js │ │ │ │ │ │ │ ├── compare.js │ │ │ │ │ │ │ ├── constructor.js │ │ │ │ │ │ │ ├── diff.js │ │ │ │ │ │ │ ├── format.js │ │ │ │ │ │ │ ├── from.js │ │ │ │ │ │ │ ├── get-set.js │ │ │ │ │ │ │ ├── locale.js │ │ │ │ │ │ │ ├── min-max.js │ │ │ │ │ │ │ ├── moment.js │ │ │ │ │ │ │ ├── prototype.js │ │ │ │ │ │ │ ├── start-end-of.js │ │ │ │ │ │ │ ├── to-type.js │ │ │ │ │ │ │ └── valid.js │ │ │ │ │ │ ├── parse/ │ │ │ │ │ │ │ ├── regex.js │ │ │ │ │ │ │ └── token.js │ │ │ │ │ │ ├── units/ │ │ │ │ │ │ │ ├── aliases.js │ │ │ │ │ │ │ ├── constants.js │ │ │ │ │ │ │ ├── day-of-month.js │ │ │ │ │ │ │ ├── day-of-week.js │ │ │ │ │ │ │ ├── day-of-year.js │ │ │ │ │ │ │ ├── hour.js │ │ │ │ │ │ │ ├── millisecond.js │ │ │ │ │ │ │ ├── minute.js │ │ │ │ │ │ │ ├── month.js │ │ │ │ │ │ │ ├── offset.js │ │ │ │ │ │ │ ├── quarter.js │ │ │ │ │ │ │ ├── second.js │ │ │ │ │ │ │ ├── timestamp.js │ │ │ │ │ │ │ ├── timezone.js │ │ │ │ │ │ │ ├── units.js │ │ │ │ │ │ │ ├── week-year.js │ │ │ │ │ │ │ ├── week.js │ │ │ │ │ │ │ └── year.js │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ ├── abs-floor.js │ │ │ │ │ │ ├── compare-arrays.js │ │ │ │ │ │ ├── defaults.js │ │ │ │ │ │ ├── deprecate.js │ │ │ │ │ │ ├── extend.js │ │ │ │ │ │ ├── has-own-prop.js │ │ │ │ │ │ ├── hooks.js │ │ │ │ │ │ ├── is-array.js │ │ │ │ │ │ ├── is-date.js │ │ │ │ │ │ ├── map.js │ │ │ │ │ │ ├── to-int.js │ │ │ │ │ │ └── zero-fill.js │ │ │ │ │ ├── locale/ │ │ │ │ │ │ ├── af.js │ │ │ │ │ │ ├── ar-ma.js │ │ │ │ │ │ ├── ar-sa.js │ │ │ │ │ │ ├── ar-tn.js │ │ │ │ │ │ ├── ar.js │ │ │ │ │ │ ├── az.js │ │ │ │ │ │ ├── be.js │ │ │ │ │ │ ├── bg.js │ │ │ │ │ │ ├── bn.js │ │ │ │ │ │ ├── bo.js │ │ │ │ │ │ ├── br.js │ │ │ │ │ │ ├── bs.js │ │ │ │ │ │ ├── ca.js │ │ │ │ │ │ ├── cs.js │ │ │ │ │ │ ├── cv.js │ │ │ │ │ │ ├── cy.js │ │ │ │ │ │ ├── da.js │ │ │ │ │ │ ├── de-at.js │ │ │ │ │ │ ├── de.js │ │ │ │ │ │ ├── el.js │ │ │ │ │ │ ├── en-au.js │ │ │ │ │ │ ├── en-ca.js │ │ │ │ │ │ ├── en-gb.js │ │ │ │ │ │ ├── eo.js │ │ │ │ │ │ ├── es.js │ │ │ │ │ │ ├── et.js │ │ │ │ │ │ ├── eu.js │ │ │ │ │ │ ├── fa.js │ │ │ │ │ │ ├── fi.js │ │ │ │ │ │ ├── fo.js │ │ │ │ │ │ ├── fr-ca.js │ │ │ │ │ │ ├── fr.js │ │ │ │ │ │ ├── fy.js │ │ │ │ │ │ ├── gl.js │ │ │ │ │ │ ├── he.js │ │ │ │ │ │ ├── hi.js │ │ │ │ │ │ ├── hr.js │ │ │ │ │ │ ├── hu.js │ │ │ │ │ │ ├── hy-am.js │ │ │ │ │ │ ├── id.js │ │ │ │ │ │ ├── is.js │ │ │ │ │ │ ├── it.js │ │ │ │ │ │ ├── ja.js │ │ │ │ │ │ ├── ka.js │ │ │ │ │ │ ├── km.js │ │ │ │ │ │ ├── ko.js │ │ │ │ │ │ ├── lb.js │ │ │ │ │ │ ├── lt.js │ │ │ │ │ │ ├── lv.js │ │ │ │ │ │ ├── mk.js │ │ │ │ │ │ ├── ml.js │ │ │ │ │ │ ├── mr.js │ │ │ │ │ │ ├── ms-my.js │ │ │ │ │ │ ├── my.js │ │ │ │ │ │ ├── nb.js │ │ │ │ │ │ ├── ne.js │ │ │ │ │ │ ├── nl.js │ │ │ │ │ │ ├── nn.js │ │ │ │ │ │ ├── pl.js │ │ │ │ │ │ ├── pt-br.js │ │ │ │ │ │ ├── pt.js │ │ │ │ │ │ ├── ro.js │ │ │ │ │ │ ├── ru.js │ │ │ │ │ │ ├── sk.js │ │ │ │ │ │ ├── sl.js │ │ │ │ │ │ ├── sq.js │ │ │ │ │ │ ├── sr-cyrl.js │ │ │ │ │ │ ├── sr.js │ │ │ │ │ │ ├── sv.js │ │ │ │ │ │ ├── ta.js │ │ │ │ │ │ ├── th.js │ │ │ │ │ │ ├── tl-ph.js │ │ │ │ │ │ ├── tr.js │ │ │ │ │ │ ├── tzm-latn.js │ │ │ │ │ │ ├── tzm.js │ │ │ │ │ │ ├── uk.js │ │ │ │ │ │ ├── uz.js │ │ │ │ │ │ ├── vi.js │ │ │ │ │ │ ├── zh-cn.js │ │ │ │ │ │ └── zh-tw.js │ │ │ │ │ └── moment.js │ │ │ │ └── templates/ │ │ │ │ ├── amd-named.js │ │ │ │ ├── amd.js │ │ │ │ ├── globals.js │ │ │ │ ├── locale-header.js │ │ │ │ └── test-header.js │ │ │ ├── ng-file-upload/ │ │ │ │ ├── .bower.json │ │ │ │ ├── FileAPI.flash.swf │ │ │ │ ├── FileAPI.js │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── angular-file-upload-all.js │ │ │ │ ├── angular-file-upload-shim.js │ │ │ │ ├── angular-file-upload.js │ │ │ │ ├── bower.json │ │ │ │ └── ng-file-upload-all.js │ │ │ ├── ngInfiniteScroll/ │ │ │ │ ├── .bower.json │ │ │ │ ├── LICENSE │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ ├── build/ │ │ │ │ │ └── ng-infinite-scroll.js │ │ │ │ ├── package.json │ │ │ │ └── src/ │ │ │ │ └── infinite-scroll.coffee │ │ │ ├── ngtoast/ │ │ │ │ ├── .bower.json │ │ │ │ ├── README.md │ │ │ │ ├── bower.json │ │ │ │ └── dist/ │ │ │ │ ├── ngToast-animations.css │ │ │ │ ├── ngToast.css │ │ │ │ └── ngToast.js │ │ │ └── openlayers/ │ │ │ ├── .bower.json │ │ │ ├── .gitignore │ │ │ ├── apidoc_config/ │ │ │ │ ├── Languages.txt │ │ │ │ ├── Menu.txt │ │ │ │ ├── OL.css │ │ │ │ └── Topics.txt │ │ │ ├── authors.txt │ │ │ ├── build/ │ │ │ │ ├── README.txt │ │ │ │ ├── build.py │ │ │ │ ├── buildUncompressed.py │ │ │ │ ├── closure-compiler/ │ │ │ │ │ └── Externs.js │ │ │ │ ├── full.cfg │ │ │ │ ├── license.txt │ │ │ │ ├── light.cfg │ │ │ │ ├── lite.cfg │ │ │ │ ├── mobile.cfg │ │ │ │ └── tests.cfg │ │ │ ├── doc_config/ │ │ │ │ ├── Languages.txt │ │ │ │ ├── Menu.txt │ │ │ │ ├── OL.css │ │ │ │ └── Topics.txt │ │ │ ├── lib/ │ │ │ │ ├── Firebug/ │ │ │ │ │ ├── firebug.css │ │ │ │ │ ├── firebug.html │ │ │ │ │ ├── firebug.js │ │ │ │ │ ├── firebugx.js │ │ │ │ │ ├── license.txt │ │ │ │ │ └── readme.txt │ │ │ │ ├── OpenLayers/ │ │ │ │ │ ├── Animation.js │ │ │ │ │ ├── BaseTypes/ │ │ │ │ │ │ ├── Bounds.js │ │ │ │ │ │ ├── Class.js │ │ │ │ │ │ ├── Date.js │ │ │ │ │ │ ├── Element.js │ │ │ │ │ │ ├── LonLat.js │ │ │ │ │ │ ├── Pixel.js │ │ │ │ │ │ └── Size.js │ │ │ │ │ ├── BaseTypes.js │ │ │ │ │ ├── Console.js │ │ │ │ │ ├── Control/ │ │ │ │ │ │ ├── ArgParser.js │ │ │ │ │ │ ├── Attribution.js │ │ │ │ │ │ ├── Button.js │ │ │ │ │ │ ├── CacheRead.js │ │ │ │ │ │ ├── CacheWrite.js │ │ │ │ │ │ ├── DragFeature.js │ │ │ │ │ │ ├── DragPan.js │ │ │ │ │ │ ├── DrawFeature.js │ │ │ │ │ │ ├── EditingToolbar.js │ │ │ │ │ │ ├── Geolocate.js │ │ │ │ │ │ ├── GetFeature.js │ │ │ │ │ │ ├── Graticule.js │ │ │ │ │ │ ├── KeyboardDefaults.js │ │ │ │ │ │ ├── LayerSwitcher.js │ │ │ │ │ │ ├── Measure.js │ │ │ │ │ │ ├── ModifyFeature/ │ │ │ │ │ │ │ └── BySegment.js │ │ │ │ │ │ ├── ModifyFeature.js │ │ │ │ │ │ ├── MousePosition.js │ │ │ │ │ │ ├── NavToolbar.js │ │ │ │ │ │ ├── Navigation.js │ │ │ │ │ │ ├── NavigationHistory.js │ │ │ │ │ │ ├── OverviewMap.js │ │ │ │ │ │ ├── Pan.js │ │ │ │ │ │ ├── PanPanel.js │ │ │ │ │ │ ├── PanZoom.js │ │ │ │ │ │ ├── PanZoomBar.js │ │ │ │ │ │ ├── Panel.js │ │ │ │ │ │ ├── Permalink.js │ │ │ │ │ │ ├── PinchZoom.js │ │ │ │ │ │ ├── SLDSelect.js │ │ │ │ │ │ ├── Scale.js │ │ │ │ │ │ ├── ScaleLine.js │ │ │ │ │ │ ├── SelectFeature.js │ │ │ │ │ │ ├── Snapping.js │ │ │ │ │ │ ├── Split.js │ │ │ │ │ │ ├── TextButtonPanel.js │ │ │ │ │ │ ├── TouchNavigation.js │ │ │ │ │ │ ├── TransformFeature.js │ │ │ │ │ │ ├── UTFGrid.js │ │ │ │ │ │ ├── WMSGetFeatureInfo.js │ │ │ │ │ │ ├── WMTSGetFeatureInfo.js │ │ │ │ │ │ ├── Zoom.js │ │ │ │ │ │ ├── ZoomBox.js │ │ │ │ │ │ ├── ZoomIn.js │ │ │ │ │ │ ├── ZoomOut.js │ │ │ │ │ │ ├── ZoomPanel.js │ │ │ │ │ │ └── ZoomToMaxExtent.js │ │ │ │ │ ├── Control.js │ │ │ │ │ ├── Events/ │ │ │ │ │ │ ├── buttonclick.js │ │ │ │ │ │ └── featureclick.js │ │ │ │ │ ├── Events.js │ │ │ │ │ ├── Feature/ │ │ │ │ │ │ └── Vector.js │ │ │ │ │ ├── Feature.js │ │ │ │ │ ├── Filter/ │ │ │ │ │ │ ├── Comparison.js │ │ │ │ │ │ ├── FeatureId.js │ │ │ │ │ │ ├── Function.js │ │ │ │ │ │ ├── Logical.js │ │ │ │ │ │ └── Spatial.js │ │ │ │ │ ├── Filter.js │ │ │ │ │ ├── Format/ │ │ │ │ │ │ ├── ArcXML/ │ │ │ │ │ │ │ └── Features.js │ │ │ │ │ │ ├── ArcXML.js │ │ │ │ │ │ ├── Atom.js │ │ │ │ │ │ ├── CQL.js │ │ │ │ │ │ ├── CSWGetDomain/ │ │ │ │ │ │ │ └── v2_0_2.js │ │ │ │ │ │ ├── CSWGetDomain.js │ │ │ │ │ │ ├── CSWGetRecords/ │ │ │ │ │ │ │ └── v2_0_2.js │ │ │ │ │ │ ├── CSWGetRecords.js │ │ │ │ │ │ ├── Context.js │ │ │ │ │ │ ├── EncodedPolyline.js │ │ │ │ │ │ ├── Filter/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ ├── v1_1_0.js │ │ │ │ │ │ │ ├── v2.js │ │ │ │ │ │ │ └── v2_0_0.js │ │ │ │ │ │ ├── Filter.js │ │ │ │ │ │ ├── GML/ │ │ │ │ │ │ │ ├── Base.js │ │ │ │ │ │ │ ├── v2.js │ │ │ │ │ │ │ └── v3.js │ │ │ │ │ │ ├── GML.js │ │ │ │ │ │ ├── GPX.js │ │ │ │ │ │ ├── GeoJSON.js │ │ │ │ │ │ ├── GeoRSS.js │ │ │ │ │ │ ├── JSON.js │ │ │ │ │ │ ├── KML.js │ │ │ │ │ │ ├── OGCExceptionReport.js │ │ │ │ │ │ ├── OSM.js │ │ │ │ │ │ ├── OWSCommon/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ └── v1_1_0.js │ │ │ │ │ │ ├── OWSCommon.js │ │ │ │ │ │ ├── OWSContext/ │ │ │ │ │ │ │ └── v0_3_1.js │ │ │ │ │ │ ├── OWSContext.js │ │ │ │ │ │ ├── QueryStringFilter.js │ │ │ │ │ │ ├── SLD/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ └── v1_0_0_GeoServer.js │ │ │ │ │ │ ├── SLD.js │ │ │ │ │ │ ├── SOSCapabilities/ │ │ │ │ │ │ │ └── v1_0_0.js │ │ │ │ │ │ ├── SOSCapabilities.js │ │ │ │ │ │ ├── SOSGetFeatureOfInterest.js │ │ │ │ │ │ ├── SOSGetObservation.js │ │ │ │ │ │ ├── TMSCapabilities.js │ │ │ │ │ │ ├── Text.js │ │ │ │ │ │ ├── WCSCapabilities/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ └── v1_1_0.js │ │ │ │ │ │ ├── WCSCapabilities.js │ │ │ │ │ │ ├── WCSDescribeCoverage/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ └── v1_1_0.js │ │ │ │ │ │ ├── WCSDescribeCoverage.js │ │ │ │ │ │ ├── WCSGetCoverage.js │ │ │ │ │ │ ├── WFS.js │ │ │ │ │ │ ├── WFSCapabilities/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ ├── v1_1_0.js │ │ │ │ │ │ │ └── v2_0_0.js │ │ │ │ │ │ ├── WFSCapabilities.js │ │ │ │ │ │ ├── WFSDescribeFeatureType.js │ │ │ │ │ │ ├── WFST/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ ├── v1_1_0.js │ │ │ │ │ │ │ └── v2_0_0.js │ │ │ │ │ │ ├── WFST.js │ │ │ │ │ │ ├── WKT.js │ │ │ │ │ │ ├── WMC/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ └── v1_1_0.js │ │ │ │ │ │ ├── WMC.js │ │ │ │ │ │ ├── WMSCapabilities/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_1.js │ │ │ │ │ │ │ ├── v1_1_0.js │ │ │ │ │ │ │ ├── v1_1_1.js │ │ │ │ │ │ │ ├── v1_1_1_WMSC.js │ │ │ │ │ │ │ ├── v1_3.js │ │ │ │ │ │ │ └── v1_3_0.js │ │ │ │ │ │ ├── WMSCapabilities.js │ │ │ │ │ │ ├── WMSDescribeLayer/ │ │ │ │ │ │ │ └── v1_1.js │ │ │ │ │ │ ├── WMSDescribeLayer.js │ │ │ │ │ │ ├── WMSGetFeatureInfo.js │ │ │ │ │ │ ├── WMTSCapabilities/ │ │ │ │ │ │ │ └── v1_0_0.js │ │ │ │ │ │ ├── WMTSCapabilities.js │ │ │ │ │ │ ├── WPSCapabilities/ │ │ │ │ │ │ │ └── v1_0_0.js │ │ │ │ │ │ ├── WPSCapabilities.js │ │ │ │ │ │ ├── WPSDescribeProcess/ │ │ │ │ │ │ │ └── v1_0_0.js │ │ │ │ │ │ ├── WPSDescribeProcess.js │ │ │ │ │ │ ├── WPSExecute.js │ │ │ │ │ │ ├── XLS/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ └── v1_1_0.js │ │ │ │ │ │ ├── XLS.js │ │ │ │ │ │ ├── XML/ │ │ │ │ │ │ │ └── VersionedOGC.js │ │ │ │ │ │ └── XML.js │ │ │ │ │ ├── Format.js │ │ │ │ │ ├── Geometry/ │ │ │ │ │ │ ├── Collection.js │ │ │ │ │ │ ├── Curve.js │ │ │ │ │ │ ├── LineString.js │ │ │ │ │ │ ├── LinearRing.js │ │ │ │ │ │ ├── MultiLineString.js │ │ │ │ │ │ ├── MultiPoint.js │ │ │ │ │ │ ├── MultiPolygon.js │ │ │ │ │ │ ├── Point.js │ │ │ │ │ │ └── Polygon.js │ │ │ │ │ ├── Geometry.js │ │ │ │ │ ├── Handler/ │ │ │ │ │ │ ├── Box.js │ │ │ │ │ │ ├── Click.js │ │ │ │ │ │ ├── Drag.js │ │ │ │ │ │ ├── Feature.js │ │ │ │ │ │ ├── Hover.js │ │ │ │ │ │ ├── Keyboard.js │ │ │ │ │ │ ├── MouseWheel.js │ │ │ │ │ │ ├── Path.js │ │ │ │ │ │ ├── Pinch.js │ │ │ │ │ │ ├── Point.js │ │ │ │ │ │ ├── Polygon.js │ │ │ │ │ │ └── RegularPolygon.js │ │ │ │ │ ├── Handler.js │ │ │ │ │ ├── Icon.js │ │ │ │ │ ├── Kinetic.js │ │ │ │ │ ├── Lang/ │ │ │ │ │ │ ├── ar.js │ │ │ │ │ │ ├── be-tarask.js │ │ │ │ │ │ ├── bg.js │ │ │ │ │ │ ├── br.js │ │ │ │ │ │ ├── ca.js │ │ │ │ │ │ ├── cs-CZ.js │ │ │ │ │ │ ├── da-DK.js │ │ │ │ │ │ ├── de.js │ │ │ │ │ │ ├── el.js │ │ │ │ │ │ ├── en-CA.js │ │ │ │ │ │ ├── en.js │ │ │ │ │ │ ├── es.js │ │ │ │ │ │ ├── fi.js │ │ │ │ │ │ ├── fr.js │ │ │ │ │ │ ├── fur.js │ │ │ │ │ │ ├── gl.js │ │ │ │ │ │ ├── gsw.js │ │ │ │ │ │ ├── hr.js │ │ │ │ │ │ ├── hsb.js │ │ │ │ │ │ ├── hu.js │ │ │ │ │ │ ├── ia.js │ │ │ │ │ │ ├── id.js │ │ │ │ │ │ ├── io.js │ │ │ │ │ │ ├── is.js │ │ │ │ │ │ ├── it.js │ │ │ │ │ │ ├── ja.js │ │ │ │ │ │ ├── km.js │ │ │ │ │ │ ├── ksh.js │ │ │ │ │ │ ├── lt.js │ │ │ │ │ │ ├── nb.js │ │ │ │ │ │ ├── nds.js │ │ │ │ │ │ ├── nl.js │ │ │ │ │ │ ├── nn.js │ │ │ │ │ │ ├── oc.js │ │ │ │ │ │ ├── pl.js │ │ │ │ │ │ ├── pt-BR.js │ │ │ │ │ │ ├── pt.js │ │ │ │ │ │ ├── ro.js │ │ │ │ │ │ ├── ru.js │ │ │ │ │ │ ├── sk.js │ │ │ │ │ │ ├── sv-SE.js │ │ │ │ │ │ ├── te.js │ │ │ │ │ │ ├── vi.js │ │ │ │ │ │ ├── zh-CN.js │ │ │ │ │ │ └── zh-TW.js │ │ │ │ │ ├── Lang.js │ │ │ │ │ ├── Layer/ │ │ │ │ │ │ ├── ArcGIS93Rest.js │ │ │ │ │ │ ├── ArcGISCache.js │ │ │ │ │ │ ├── ArcIMS.js │ │ │ │ │ │ ├── Bing.js │ │ │ │ │ │ ├── Boxes.js │ │ │ │ │ │ ├── EventPane.js │ │ │ │ │ │ ├── FixedZoomLevels.js │ │ │ │ │ │ ├── GeoRSS.js │ │ │ │ │ │ ├── Google/ │ │ │ │ │ │ │ └── v3.js │ │ │ │ │ │ ├── Google.js │ │ │ │ │ │ ├── Grid.js │ │ │ │ │ │ ├── HTTPRequest.js │ │ │ │ │ │ ├── Image.js │ │ │ │ │ │ ├── KaMap.js │ │ │ │ │ │ ├── KaMapCache.js │ │ │ │ │ │ ├── MapGuide.js │ │ │ │ │ │ ├── MapServer.js │ │ │ │ │ │ ├── Markers.js │ │ │ │ │ │ ├── OSM.js │ │ │ │ │ │ ├── PointGrid.js │ │ │ │ │ │ ├── PointTrack.js │ │ │ │ │ │ ├── SphericalMercator.js │ │ │ │ │ │ ├── TMS.js │ │ │ │ │ │ ├── Text.js │ │ │ │ │ │ ├── TileCache.js │ │ │ │ │ │ ├── UTFGrid.js │ │ │ │ │ │ ├── Vector/ │ │ │ │ │ │ │ └── RootContainer.js │ │ │ │ │ │ ├── Vector.js │ │ │ │ │ │ ├── WMS.js │ │ │ │ │ │ ├── WMTS.js │ │ │ │ │ │ ├── WorldWind.js │ │ │ │ │ │ ├── XYZ.js │ │ │ │ │ │ └── Zoomify.js │ │ │ │ │ ├── Layer.js │ │ │ │ │ ├── Map.js │ │ │ │ │ ├── Marker/ │ │ │ │ │ │ └── Box.js │ │ │ │ │ ├── Marker.js │ │ │ │ │ ├── Popup/ │ │ │ │ │ │ ├── Anchored.js │ │ │ │ │ │ ├── Framed.js │ │ │ │ │ │ └── FramedCloud.js │ │ │ │ │ ├── Popup.js │ │ │ │ │ ├── Projection.js │ │ │ │ │ ├── Protocol/ │ │ │ │ │ │ ├── CSW/ │ │ │ │ │ │ │ └── v2_0_2.js │ │ │ │ │ │ ├── CSW.js │ │ │ │ │ │ ├── HTTP.js │ │ │ │ │ │ ├── SOS/ │ │ │ │ │ │ │ └── v1_0_0.js │ │ │ │ │ │ ├── SOS.js │ │ │ │ │ │ ├── Script.js │ │ │ │ │ │ ├── WFS/ │ │ │ │ │ │ │ ├── v1.js │ │ │ │ │ │ │ ├── v1_0_0.js │ │ │ │ │ │ │ ├── v1_1_0.js │ │ │ │ │ │ │ └── v2_0_0.js │ │ │ │ │ │ └── WFS.js │ │ │ │ │ ├── Protocol.js │ │ │ │ │ ├── Renderer/ │ │ │ │ │ │ ├── Canvas.js │ │ │ │ │ │ ├── Elements.js │ │ │ │ │ │ ├── SVG.js │ │ │ │ │ │ └── VML.js │ │ │ │ │ ├── Renderer.js │ │ │ │ │ ├── Request/ │ │ │ │ │ │ └── XMLHttpRequest.js │ │ │ │ │ ├── Request.js │ │ │ │ │ ├── Rule.js │ │ │ │ │ ├── SingleFile.js │ │ │ │ │ ├── Spherical.js │ │ │ │ │ ├── Strategy/ │ │ │ │ │ │ ├── BBOX.js │ │ │ │ │ │ ├── Cluster.js │ │ │ │ │ │ ├── Filter.js │ │ │ │ │ │ ├── Fixed.js │ │ │ │ │ │ ├── Paging.js │ │ │ │ │ │ ├── Refresh.js │ │ │ │ │ │ └── Save.js │ │ │ │ │ ├── Strategy.js │ │ │ │ │ ├── Style.js │ │ │ │ │ ├── Style2.js │ │ │ │ │ ├── StyleMap.js │ │ │ │ │ ├── Symbolizer/ │ │ │ │ │ │ ├── Line.js │ │ │ │ │ │ ├── Point.js │ │ │ │ │ │ ├── Polygon.js │ │ │ │ │ │ ├── Raster.js │ │ │ │ │ │ └── Text.js │ │ │ │ │ ├── Symbolizer.js │ │ │ │ │ ├── Tile/ │ │ │ │ │ │ ├── Image/ │ │ │ │ │ │ │ └── IFrame.js │ │ │ │ │ │ ├── Image.js │ │ │ │ │ │ └── UTFGrid.js │ │ │ │ │ ├── Tile.js │ │ │ │ │ ├── TileManager.js │ │ │ │ │ ├── Tween.js │ │ │ │ │ ├── Util/ │ │ │ │ │ │ └── vendorPrefix.js │ │ │ │ │ ├── Util.js │ │ │ │ │ ├── WPSClient.js │ │ │ │ │ └── WPSProcess.js │ │ │ │ ├── OpenLayers.js │ │ │ │ ├── Rico/ │ │ │ │ │ ├── Color.js │ │ │ │ │ ├── Corner.js │ │ │ │ │ └── license.js │ │ │ │ └── deprecated.js │ │ │ ├── license.txt │ │ │ ├── licenses/ │ │ │ │ ├── APACHE-2.0.txt │ │ │ │ ├── BSD-LICENSE.txt │ │ │ │ └── MIT-LICENSE.txt │ │ │ ├── notes/ │ │ │ │ ├── 2.12.md │ │ │ │ ├── 2.13.md │ │ │ │ └── 2.14.md │ │ │ ├── readme.md │ │ │ ├── tests/ │ │ │ │ ├── Animation.html │ │ │ │ ├── BaseTypes/ │ │ │ │ │ ├── Bounds.html │ │ │ │ │ ├── Class.html │ │ │ │ │ ├── Date.html │ │ │ │ │ ├── Element.html │ │ │ │ │ ├── LonLat.html │ │ │ │ │ ├── Pixel.html │ │ │ │ │ └── Size.html │ │ │ │ ├── BaseTypes.html │ │ │ │ ├── Console.html │ │ │ │ ├── Control/ │ │ │ │ │ ├── ArgParser.html │ │ │ │ │ ├── Attribution.html │ │ │ │ │ ├── Button.html │ │ │ │ │ ├── CacheRead.html │ │ │ │ │ ├── CacheWrite.html │ │ │ │ │ ├── DragFeature.html │ │ │ │ │ ├── DragPan.html │ │ │ │ │ ├── DrawFeature.html │ │ │ │ │ ├── EditingToolbar.html │ │ │ │ │ ├── Geolocate.html │ │ │ │ │ ├── GetFeature.html │ │ │ │ │ ├── Graticule.html │ │ │ │ │ ├── KeyboardDefaults.html │ │ │ │ │ ├── LayerSwitcher.html │ │ │ │ │ ├── Measure.html │ │ │ │ │ ├── ModifyFeature/ │ │ │ │ │ │ └── BySegment.html │ │ │ │ │ ├── ModifyFeature.html │ │ │ │ │ ├── MousePosition.html │ │ │ │ │ ├── NavToolbar.html │ │ │ │ │ ├── Navigation.html │ │ │ │ │ ├── NavigationHistory.html │ │ │ │ │ ├── OverviewMap.html │ │ │ │ │ ├── Pan.html │ │ │ │ │ ├── PanPanel.html │ │ │ │ │ ├── PanZoom.html │ │ │ │ │ ├── PanZoomBar.html │ │ │ │ │ ├── Panel.html │ │ │ │ │ ├── Permalink.html │ │ │ │ │ ├── PinchZoom.html │ │ │ │ │ ├── SLDSelect.html │ │ │ │ │ ├── Scale.html │ │ │ │ │ ├── ScaleLine.html │ │ │ │ │ ├── SelectFeature.html │ │ │ │ │ ├── Snapping.html │ │ │ │ │ ├── Split.html │ │ │ │ │ ├── TextButtonPanel.html │ │ │ │ │ ├── TouchNavigation.html │ │ │ │ │ ├── TransformFeature.html │ │ │ │ │ ├── UTFGrid.html │ │ │ │ │ ├── WMSGetFeatureInfo.html │ │ │ │ │ ├── WMTSGetFeatureInfo.html │ │ │ │ │ ├── Zoom.html │ │ │ │ │ ├── ZoomBox.html │ │ │ │ │ ├── ZoomIn.html │ │ │ │ │ ├── ZoomOut.html │ │ │ │ │ └── ZoomToMaxExtent.html │ │ │ │ ├── Control.html │ │ │ │ ├── Events/ │ │ │ │ │ ├── buttonclick.html │ │ │ │ │ └── featureclick.html │ │ │ │ ├── Events.html │ │ │ │ ├── Extras.html │ │ │ │ ├── Feature/ │ │ │ │ │ └── Vector.html │ │ │ │ ├── Feature.html │ │ │ │ ├── Filter/ │ │ │ │ │ ├── Comparison.html │ │ │ │ │ ├── FeatureId.html │ │ │ │ │ ├── Logical.html │ │ │ │ │ └── Spatial.html │ │ │ │ ├── Filter.html │ │ │ │ ├── Format/ │ │ │ │ │ ├── ArcXML/ │ │ │ │ │ │ └── Features.html │ │ │ │ │ ├── ArcXML.html │ │ │ │ │ ├── Atom.html │ │ │ │ │ ├── CQL.html │ │ │ │ │ ├── CSWGetDomain/ │ │ │ │ │ │ ├── v2_0_2.html │ │ │ │ │ │ └── v2_0_2.js │ │ │ │ │ ├── CSWGetDomain.html │ │ │ │ │ ├── CSWGetRecords/ │ │ │ │ │ │ ├── v2_0_2.html │ │ │ │ │ │ └── v2_0_2.js │ │ │ │ │ ├── CSWGetRecords.html │ │ │ │ │ ├── EncodedPolyline.html │ │ │ │ │ ├── Filter/ │ │ │ │ │ │ ├── v1.html │ │ │ │ │ │ ├── v1_0_0.html │ │ │ │ │ │ ├── v1_1_0.html │ │ │ │ │ │ └── v2_0_0.html │ │ │ │ │ ├── Filter.html │ │ │ │ │ ├── GML/ │ │ │ │ │ │ ├── cases.js │ │ │ │ │ │ ├── v2.html │ │ │ │ │ │ └── v3.html │ │ │ │ │ ├── GML.html │ │ │ │ │ ├── GPX.html │ │ │ │ │ ├── GeoJSON.html │ │ │ │ │ ├── GeoRSS.html │ │ │ │ │ ├── JSON.html │ │ │ │ │ ├── KML.html │ │ │ │ │ ├── OGCExceptionReport.html │ │ │ │ │ ├── OSM.html │ │ │ │ │ ├── OWSCommon/ │ │ │ │ │ │ ├── v1_0_0.html │ │ │ │ │ │ └── v1_1_0.html │ │ │ │ │ ├── OWSContext/ │ │ │ │ │ │ └── v0_3_1.html │ │ │ │ │ ├── QueryStringFilter.html │ │ │ │ │ ├── SLD/ │ │ │ │ │ │ ├── v1_0_0.html │ │ │ │ │ │ └── v1_0_0_GeoServer.html │ │ │ │ │ ├── SLD.html │ │ │ │ │ ├── SOSCapabilities/ │ │ │ │ │ │ ├── v1_0_0.html │ │ │ │ │ │ └── v1_0_0.js │ │ │ │ │ ├── SOSGetFeatureOfInterest.html │ │ │ │ │ ├── SOSGetObservation.html │ │ │ │ │ ├── TMSCapabilities.html │ │ │ │ │ ├── Text.html │ │ │ │ │ ├── WCSCapabilities/ │ │ │ │ │ │ └── v1.html │ │ │ │ │ ├── WCSCapabilities.html │ │ │ │ │ ├── WCSDescribeCoverage/ │ │ │ │ │ │ └── v1.html │ │ │ │ │ ├── WCSDescribeCoverage.html │ │ │ │ │ ├── WCSGetCoverage.html │ │ │ │ │ ├── WFS.html │ │ │ │ │ ├── WFSCapabilities/ │ │ │ │ │ │ ├── v1.html │ │ │ │ │ │ └── v2.html │ │ │ │ │ ├── WFSCapabilities.html │ │ │ │ │ ├── WFSDescribeFeatureType.html │ │ │ │ │ ├── WFST/ │ │ │ │ │ │ ├── v1.html │ │ │ │ │ │ ├── v1_0_0.html │ │ │ │ │ │ ├── v1_1_0.html │ │ │ │ │ │ └── v2_0_0.html │ │ │ │ │ ├── WFST.html │ │ │ │ │ ├── WKT.html │ │ │ │ │ ├── WMC/ │ │ │ │ │ │ ├── v1.html │ │ │ │ │ │ └── v1_1_0.html │ │ │ │ │ ├── WMC.html │ │ │ │ │ ├── WMSCapabilities/ │ │ │ │ │ │ ├── v1_1_1.html │ │ │ │ │ │ ├── v1_1_1_WMSC.html │ │ │ │ │ │ └── v1_3_0.html │ │ │ │ │ ├── WMSCapabilities.html │ │ │ │ │ ├── WMSDescribeLayer.html │ │ │ │ │ ├── WMSGetFeatureInfo.html │ │ │ │ │ ├── WMTSCapabilities/ │ │ │ │ │ │ └── v1_0_0.html │ │ │ │ │ ├── WMTSCapabilities.html │ │ │ │ │ ├── WPSCapabilities/ │ │ │ │ │ │ ├── v1_0_0.html │ │ │ │ │ │ └── v1_0_0.js │ │ │ │ │ ├── WPSDescribeProcess.html │ │ │ │ │ ├── WPSExecute.html │ │ │ │ │ ├── XLS/ │ │ │ │ │ │ └── v1_1_0.html │ │ │ │ │ ├── XML/ │ │ │ │ │ │ └── VersionedOGC.html │ │ │ │ │ └── XML.html │ │ │ │ ├── Format.html │ │ │ │ ├── Geometry/ │ │ │ │ │ ├── Collection.html │ │ │ │ │ ├── Curve.html │ │ │ │ │ ├── LineString.html │ │ │ │ │ ├── LinearRing.html │ │ │ │ │ ├── MultiLineString.html │ │ │ │ │ ├── MultiPoint.html │ │ │ │ │ ├── MultiPolygon.html │ │ │ │ │ ├── Point.html │ │ │ │ │ └── Polygon.html │ │ │ │ ├── Geometry.html │ │ │ │ ├── Handler/ │ │ │ │ │ ├── Box.html │ │ │ │ │ ├── Click.html │ │ │ │ │ ├── Drag.html │ │ │ │ │ ├── Feature.html │ │ │ │ │ ├── Hover.html │ │ │ │ │ ├── Keyboard.html │ │ │ │ │ ├── MouseWheel.html │ │ │ │ │ ├── Path.html │ │ │ │ │ ├── Pinch.html │ │ │ │ │ ├── Point.html │ │ │ │ │ ├── Polygon.html │ │ │ │ │ └── RegularPolygon.html │ │ │ │ ├── Handler.html │ │ │ │ ├── Icon.html │ │ │ │ ├── Kinetic.html │ │ │ │ ├── Lang.html │ │ │ │ ├── Layer/ │ │ │ │ │ ├── ArcGIS93Rest.html │ │ │ │ │ ├── ArcGISCache.html │ │ │ │ │ ├── ArcGISCache.json │ │ │ │ │ ├── ArcIMS.html │ │ │ │ │ ├── Bing.html │ │ │ │ │ ├── EventPane.html │ │ │ │ │ ├── FixedZoomLevels.html │ │ │ │ │ ├── GeoRSS.html │ │ │ │ │ ├── Google/ │ │ │ │ │ │ └── v3.html │ │ │ │ │ ├── Grid.html │ │ │ │ │ ├── HTTPRequest.html │ │ │ │ │ ├── Image.html │ │ │ │ │ ├── KaMap.html │ │ │ │ │ ├── MapGuide.html │ │ │ │ │ ├── MapServer.html │ │ │ │ │ ├── Markers.html │ │ │ │ │ ├── OSM.html │ │ │ │ │ ├── PointGrid.html │ │ │ │ │ ├── PointTrack.html │ │ │ │ │ ├── SphericalMercator.html │ │ │ │ │ ├── TMS.html │ │ │ │ │ ├── Text.html │ │ │ │ │ ├── TileCache.html │ │ │ │ │ ├── UTFGrid.html │ │ │ │ │ ├── Vector/ │ │ │ │ │ │ └── RootContainer.html │ │ │ │ │ ├── Vector.html │ │ │ │ │ ├── WMS.html │ │ │ │ │ ├── WMTS.html │ │ │ │ │ ├── WrapDateLine.html │ │ │ │ │ ├── XYZ.html │ │ │ │ │ ├── atom-1.0.xml │ │ │ │ │ ├── data_Layer_Text_textfile.txt │ │ │ │ │ ├── data_Layer_Text_textfile_2.txt │ │ │ │ │ ├── data_Layer_Text_textfile_overflow.txt │ │ │ │ │ └── georss.txt │ │ │ │ ├── Layer.html │ │ │ │ ├── Map.html │ │ │ │ ├── Marker/ │ │ │ │ │ └── Box.html │ │ │ │ ├── Marker.html │ │ │ │ ├── OLLoader.js │ │ │ │ ├── OpenLayers1.html │ │ │ │ ├── OpenLayers2.html │ │ │ │ ├── OpenLayers3.html │ │ │ │ ├── OpenLayers4.html │ │ │ │ ├── OpenLayersJsFiles.html │ │ │ │ ├── Popup/ │ │ │ │ │ ├── Anchored.html │ │ │ │ │ └── FramedCloud.html │ │ │ │ ├── Popup.html │ │ │ │ ├── Projection.html │ │ │ │ ├── Protocol/ │ │ │ │ │ ├── CSW.html │ │ │ │ │ ├── HTTP.html │ │ │ │ │ ├── SOS.html │ │ │ │ │ ├── Script.html │ │ │ │ │ └── WFS.html │ │ │ │ ├── Protocol.html │ │ │ │ ├── README.txt │ │ │ │ ├── Renderer/ │ │ │ │ │ ├── Canvas.html │ │ │ │ │ ├── Elements.html │ │ │ │ │ ├── SVG.html │ │ │ │ │ └── VML.html │ │ │ │ ├── Renderer.html │ │ │ │ ├── Request/ │ │ │ │ │ └── XMLHttpRequest.html │ │ │ │ ├── Request.html │ │ │ │ ├── Rule.html │ │ │ │ ├── SingleFile1.html │ │ │ │ ├── SingleFile2.html │ │ │ │ ├── SingleFile3.html │ │ │ │ ├── Strategy/ │ │ │ │ │ ├── BBOX.html │ │ │ │ │ ├── Cluster.html │ │ │ │ │ ├── Filter.html │ │ │ │ │ ├── Fixed.html │ │ │ │ │ ├── Paging.html │ │ │ │ │ ├── Refresh.html │ │ │ │ │ └── Save.html │ │ │ │ ├── Strategy.html │ │ │ │ ├── Style.html │ │ │ │ ├── Style2.html │ │ │ │ ├── StyleMap.html │ │ │ │ ├── Symbolizer/ │ │ │ │ │ ├── Line.html │ │ │ │ │ ├── Point.html │ │ │ │ │ ├── Polygon.html │ │ │ │ │ ├── Raster.html │ │ │ │ │ └── Text.html │ │ │ │ ├── Symbolizer.html │ │ │ │ ├── Test.AnotherWay.baseadditions.js │ │ │ │ ├── Test.AnotherWay.css │ │ │ │ ├── Test.AnotherWay.geom_eq.js │ │ │ │ ├── Test.AnotherWay.js │ │ │ │ ├── Test.AnotherWay.xml_eq.js │ │ │ │ ├── Tile/ │ │ │ │ │ ├── Image/ │ │ │ │ │ │ └── IFrame.html │ │ │ │ │ ├── Image.html │ │ │ │ │ └── UTFGrid.html │ │ │ │ ├── Tile.html │ │ │ │ ├── TileManager.html │ │ │ │ ├── Tween.html │ │ │ │ ├── Util/ │ │ │ │ │ └── vendorPrefix.html │ │ │ │ ├── Util.html │ │ │ │ ├── Util_common.js │ │ │ │ ├── Util_w3c.html │ │ │ │ ├── WPSClient.html │ │ │ │ ├── WPSProcess.html │ │ │ │ ├── atom-1.0.xml │ │ │ │ ├── auto-tests.html │ │ │ │ ├── data_Layer_Text_textfile.txt │ │ │ │ ├── data_Layer_Text_textfile_2.txt │ │ │ │ ├── data_Layer_Text_textfile_overflow.txt │ │ │ │ ├── deprecated/ │ │ │ │ │ ├── Ajax.html │ │ │ │ │ ├── BaseTypes/ │ │ │ │ │ │ ├── Class.html │ │ │ │ │ │ └── Element.html │ │ │ │ │ ├── Control/ │ │ │ │ │ │ └── MouseToolbar.html │ │ │ │ │ ├── Geometry/ │ │ │ │ │ │ └── Rectangle.html │ │ │ │ │ ├── Layer/ │ │ │ │ │ │ ├── GML.html │ │ │ │ │ │ ├── MapServer/ │ │ │ │ │ │ │ └── Untiled.html │ │ │ │ │ │ ├── MapServer.html │ │ │ │ │ │ ├── WFS.html │ │ │ │ │ │ ├── WMS/ │ │ │ │ │ │ │ └── Post.html │ │ │ │ │ │ ├── WMS.html │ │ │ │ │ │ ├── Yahoo.html │ │ │ │ │ │ ├── mice.xml │ │ │ │ │ │ └── owls.xml │ │ │ │ │ ├── Popup/ │ │ │ │ │ │ └── AnchoredBubble.html │ │ │ │ │ ├── Protocol/ │ │ │ │ │ │ ├── SQL/ │ │ │ │ │ │ │ └── Gears.html │ │ │ │ │ │ └── SQL.html │ │ │ │ │ ├── Renderer/ │ │ │ │ │ │ └── SVG2.html │ │ │ │ │ ├── Tile/ │ │ │ │ │ │ └── WFS.html │ │ │ │ │ └── Util.html │ │ │ │ ├── georss.txt │ │ │ │ ├── grid_inittiles.html │ │ │ │ ├── index.html │ │ │ │ ├── list-tests.html │ │ │ │ ├── manual/ │ │ │ │ │ ├── ajax.html │ │ │ │ │ ├── ajax.txt │ │ │ │ │ ├── alloverlays-mixed.html │ │ │ │ │ ├── arcims-2117.html │ │ │ │ │ ├── arkansas.rss │ │ │ │ │ ├── big-georss.html │ │ │ │ │ ├── box-quirks.html │ │ │ │ │ ├── box-strict.html │ │ │ │ │ ├── clip-features-svg.html │ │ │ │ │ ├── dateline-sketch.html │ │ │ │ │ ├── dateline-smallextent.html │ │ │ │ │ ├── draw-feature.html │ │ │ │ │ ├── feature-handler.html │ │ │ │ │ ├── geodesic.html │ │ │ │ │ ├── geojson-geomcoll-reprojection.html │ │ │ │ │ ├── google-fullscreen-overlay.html │ │ │ │ │ ├── google-panning.html │ │ │ │ │ ├── google-resize.html │ │ │ │ │ ├── google-tilt.html │ │ │ │ │ ├── google-v3-resize.html │ │ │ │ │ ├── loadend.html │ │ │ │ │ ├── map-events.html │ │ │ │ │ ├── memory/ │ │ │ │ │ │ ├── Marker-2258.html │ │ │ │ │ │ ├── PanZoom-2323.html │ │ │ │ │ │ ├── RemoveChild-2170.html │ │ │ │ │ │ └── VML-2170.html │ │ │ │ │ ├── multiple-google-layers.html │ │ │ │ │ ├── overviewmap-projection.html │ │ │ │ │ ├── page-position.html │ │ │ │ │ ├── pan-redraw-svg.html │ │ │ │ │ ├── popup-keepInMap.html │ │ │ │ │ ├── reflow.html │ │ │ │ │ ├── renderedDimensions.html │ │ │ │ │ ├── select-feature-right-click.html │ │ │ │ │ ├── select-feature.html │ │ │ │ │ ├── tiles-loading.html │ │ │ │ │ ├── tween.html │ │ │ │ │ ├── vector-features-performance.html │ │ │ │ │ └── vector-layer-zindex.html │ │ │ │ ├── mice.xml │ │ │ │ ├── node.js/ │ │ │ │ │ ├── mockdom.js │ │ │ │ │ ├── node-tests.cfg │ │ │ │ │ ├── node.js │ │ │ │ │ ├── run-test.js │ │ │ │ │ └── run.sh │ │ │ │ ├── owls.xml │ │ │ │ ├── run-tests.html │ │ │ │ ├── selenium/ │ │ │ │ │ └── remotecontrol/ │ │ │ │ │ ├── config.cfg │ │ │ │ │ ├── selenium.py │ │ │ │ │ ├── setup.txt │ │ │ │ │ └── test_ol.py │ │ │ │ ├── speed/ │ │ │ │ │ ├── geometry.html │ │ │ │ │ ├── string_format.html │ │ │ │ │ ├── vector-renderers.html │ │ │ │ │ ├── vector-renderers.js │ │ │ │ │ ├── wmc_speed.html │ │ │ │ │ ├── wmscaps.html │ │ │ │ │ ├── wmscaps.js │ │ │ │ │ └── wmscaps.xml │ │ │ │ └── throws.js │ │ │ ├── theme/ │ │ │ │ └── default/ │ │ │ │ ├── google.css │ │ │ │ ├── ie6-style.css │ │ │ │ ├── style.css │ │ │ │ └── style.mobile.css │ │ │ └── tools/ │ │ │ ├── BeautifulSoup.py │ │ │ ├── README.txt │ │ │ ├── closure_library_jscompiler.py │ │ │ ├── closure_ws.py │ │ │ ├── exampleparser.py │ │ │ ├── jsmin.c │ │ │ ├── jsmin.py │ │ │ ├── mergejs.py │ │ │ ├── minimize.py │ │ │ ├── oldot.py │ │ │ ├── release.sh │ │ │ ├── shrinksafe.py │ │ │ ├── toposort.py │ │ │ ├── uglify_js.py │ │ │ └── update_dev_dir.sh │ │ ├── css/ │ │ │ ├── ie-only.css │ │ │ └── tatami.css │ │ └── vendor/ │ │ ├── backgroundsize.min.htc │ │ ├── css/ │ │ │ └── bootstrap/ │ │ │ ├── css/ │ │ │ │ └── bootstrap.css │ │ │ ├── fonts/ │ │ │ │ └── glyphiconshalflings-regular.otf │ │ │ └── js/ │ │ │ └── bootstrap.js │ │ └── js/ │ │ ├── marked/ │ │ │ └── marked.js │ │ └── respond/ │ │ └── respond.js │ ├── css/ │ │ ├── ie-only.css │ │ ├── tatami.css │ │ └── vendor/ │ │ ├── backgroundsize.min.htc │ │ ├── css/ │ │ │ ├── bootstrap.css │ │ │ └── jQueryjGrowl.css │ │ └── fonts/ │ │ └── glyphiconshalflings-regular.otf │ ├── index.html │ ├── index.html.tpl │ ├── index.jsp │ ├── robots.txt │ └── sitemap.xml └── test/ ├── java/ │ └── fr/ │ └── ippon/ │ └── tatami/ │ └── web/ │ └── rest/ │ ├── GroupControllerTest.java │ ├── TagControllerTest.java │ ├── TimelineControllerTest.java │ └── UserControllerTest.java └── javascript/ └── webapp/ └── app/ └── components/ ├── about/ │ └── license/ │ └── LicenseController_spec.js ├── account/ │ ├── PasswordModule_spec.js │ └── preferences/ │ └── Preferences_spec.js └── home/ └── status/ └── StatusController_spec.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ data .idea *.iml tatami.iml tatami.ipr tatami.iws .classpath .project .settings target node_modules/ web/node_modules/ src/main/webapp/WEB-INF/generated-wro4j .merge_file* .DS_Store phantomjsdriver.log *.log web/src/main/webapp/app/**/*.CONCAT.js web/src/main/webapp/app/**/*.min.html web/src/main/webapp/app/**/*.min.js web/src/main/webapp/app/**/*.MIN.css web/src/main/webapp/app/**/*.min.css web/src/main/webapp/css/tatami.min.css src/main/webapp/app/shared/footer/FooterView.html.sed.del ================================================ FILE: CONTRIBUTING.md ================================================ Contributing to Tatami ========================= Open Source ------------------ Tatami is an Open Source project, which uses the Apache 2 [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) license. If you contribute to Tatami, you agree that your code belongs to the Tatami project and follows this license. Submitting code ------------------ * Create a [GitHub account](https://github.com/signup/free). * [Submit a ticket for your issue](https://github.com/ippontech/tatami/issues), assuming one does not already exist. * Clearly describe the issue including steps to reproduce it, when it is a bug. * Fork the repository on GitHub. * Make commits of logical units. * Make sure your commit messages include the ticket number you have created. Github should automatically link your changes with the ticket. * Make sure you have added the necessary tests for your changes. * Run _all_ the tests to assure nothing else was accidentally broken. * Push your changes in your fork of the repository. * Submit a pull request to [the repository in the ippontech organization](https://github.com/ippontech/tatami). * If you need help about making a pull request, [here is the documentation](https://help.github.com/articles/using-pull-requests). * Your code will be automatically built by [Buildhive](https://buildhive.cloudbees.com/job/ippontech/job/tatami/). Your pull request should have a Buildhive comment a few minutes after your commit. ================================================ FILE: README.md ================================================ Tatami ================ Presentation ------------------ Tatami is an Open Source enterprise social network. A public installation of Tatami is provided by [Ippon Technologies](http://www.ippon.fr) at : [https://tatami.ippon.fr](https://tatami.ippon.fr) Tatami is made with the following technologies : - HTML5, [AngularJS](https://angularjs.org/) and [Twitter Bootstrap](http://twitter.github.com/bootstrap/) - [The Spring Framework](http://www.springsource.org/) - [Apache Cassandra](http://cassandra.apache.org/) - [Elastic Search](http://www.elasticsearch.org/) Tatami is developed by [Ippon Technologies](http://www.ippon.fr) Installation for developers --------------------------------------- ### 5 minute installation - Clone, fork or download the source code from this Github page - Install [Maven 3](http://maven.apache.org/) - Install [npm](https://www.npmjs.com/) - Point your terminal to the directory you cloned Tatami to. - `cd web` - Type `npm install` - You may need to give root user permissions: `sudo !!` - `cd ..` - Run Cassandra with Maven : `mvn cassandra:run` - Run Jetty from tatami/web with Maven: `mvn jetty:run` - Connect to the application at http://127.0.0.1:8080 To create users, use the registration form. As we have not configured a SMTP server (you can configure it in src/main/resources/META-INF/tatami/tatami.properties - see below "installation for production use" for more options), the validation URL as well as the password will not be e-mailed to you, but you can see them in the log (look at the Jetty console output). ### Using Tomcat instead of Jetty If you want to use Tomcat instead of Jetty (which works better in development mode on Windows), just use : - Run Tomcat from Maven : `mvn tomcat7:run` ### Maven tuning and troubleshooting If you run into some Permgen or OutOfMemory errors, you can configure your Maven settings accordingly : ``` export MAVEN_OPTS="-XX:PermSize=64m -XX:MaxPermSize=128m -Xms256m -Xmx1024m" ``` If you want to debug remotely the application with your IDE, set up your MAVEN_OPTS : ``` export MAVEN_OPTS="$MAVEN_OPTS -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n" ``` ### Cassandra troubleshooting On Mac OS X, you should use JDK 6 and not JDK 7, see [issue #281](https://github.com/ippontech/tatami/issues/281#issuecomment-12430701). How to Contribute --------------------------------------- In order to assure code quality, Ippon manages the pull requests for the project, and upholds certain rules regarding how individuals may contribute. You may find these rules in your Tatami installation in the file `CONTRIBUTING.md`. Installation for production use --------------------------------------- ### Cassandra installation - Download [Apache Cassandra](http://cassandra.apache.org/) - Install Cassandra : the application will work fine with just one node, but ideally you should have a cluster with at least 3 or 5 nodes - Cassandra is configured with its cassandra.yaml file : don't forget to backup your "data" and "commitlog" directories ### Tatami installation In order to use a stable version, use one of the [available tags](https://github.com/ippontech/tatami/tags). Tatami can be configured with the src/main/resources/META-INF/tatami/tatami.properties file. You can configure this file in 2 ways : - Edit the file in your own Tatami fork - Properties in this file are replaced at build time by Maven : you can set up your own Maven profile with your specific properties Once Tatami is started, you will be able to check your properties at runtime in the Administration page. To deploy Tatami : - Create the Tatami WAR file : `mvn package` - The WAR file will be called "root.war", as Tatami should be run as the root application (on the "/" Web context) - Deploy the WAR file on your favorite Java EE server - The WAR has been tested on Jetty 8 and Tomcat 7, and should work fine on all Java EE servers Upgrading from a previous version --------------------------------------- Upgrading is normally just a matter of using a newer version of the application. Sometimes, you will need to update the Cassandra keyspace: upgrade scripts are available in the src/main/cql/upgrade directory. Launching stress tests --------------------------------------- Stress tests are done with [Apache JMeter](http://jmeter.apache.org/). - Launch Cassandra - Run Tatami from Maven with the `stress-tests` profile : `mvn jetty:run -Pstress-tests` - Launch JMeter - Run the `src/test/jmeter/tatami-create-users.jmx` script : it will create 200 normal users, which each has 200 follower users - Run the stress test : `src/test/jmeter/tatami-stress-test.jmx` Launching functional tests --------------------------------------- Functional tests are a work in progress, you do not have to run them in order to use the application. Requirement : all components must run on localhost : - for LDAP authentication, the tests starts the LDAP server that the Tatami server will use - for fixture setup and assertions, the test connects directly to the local cassandra Launching UI Tests from maven : - add this profile on your settings.xml : ```xml tatami true C:\path\to\chromedriver.exe xxxx xxx@xxx.fr ``` - Run Maven with this command : `mvn clean verify -Puitest` Launching UI Tests from maven with Chrome : - install ChromeDriver in your system - configure the property "webdriver.chrome.driver" in your settings pointing to your chrome driver install directory - add `-Dgeb.env=chrome` to the maven command above Launching UI Tests from your IDE : - Enable a groovy plugin on your IDE - Activate maven profile "uitest" or add src/integration/* in your classpath - Run Tatami with Maven : `mvn cassandra:delete cassandra:start jetty:run -Djetty.scanIntervalSeconds=0 -Puitest` - Run Specs (in src\integration\java\fr\ippon\tatami\uitest) as Junit Tests from your IDE => you have to set adequate system properties to your running configurations (the same as those that are necessary in setting.xml for maven : see above) Thanks ------ Jetbrains is providing us free [Intellij IDEA](http://www.jetbrains.com/idea/) licenses, which definitely allows us to be more productive and have more fun on the project! YourKit is kindly supporting open source projects with its full-featured Java Profiler. YourKit, LLC is the creator of innovative and intelligent tools for profiling Java and .NET applications. Take a look at YourKit's leading software products: [YourKit Java Profiler](http://www.yourkit.com/java/profiler/index.jsp) and [YourKit .NET Profiler](http://www.yourkit.com/.net/profiler/index.jsp). License ------- Copyright © 2012-2015 [Ippon Technologies](http://www.ippon.fr) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this application except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: etc/installation/ubuntu/files/maven/settings.xml ================================================ /opt/tatami/maven/repository ================================================ FILE: etc/installation/ubuntu/install.sh ================================================ #!/bin/sh # # description: Installs Tatami on Ubuntu # This script must be run by the "root" user. # Run this script directly by typing : # curl -L https://github.com/ippontech/tatami/raw/master/etc/installation/ubuntu/install.sh | sudo bash # # - Tatami is installed in the "/opt/tatami" directory # - Tatami is run by the "tatami" user # echo "Welcome to the Tatami installer" ################################# # Variables ################################# echo "Setting up variables" export USER=tatami export TATAMI_DIR=/opt/tatami export MAVEN_VERSION=3.0.4 export JETTY_VERSION=8.1.8.v20121106 ################################# # Install missing packages ################################# echo "Installing missing packages" apt-get install git-core curl wget -y --force-yes ################################# # Install Java ################################# wget https://github.com/flexiondotorg/oab-java6/raw/0.2.6/oab-java.sh -O oab-java.sh chmod +x oab-java.sh sudo ./oab-java.sh rm oab-java.sh sudo apt-get install sun-java6-jdk -y ################################# # Create directories & users ################################# echo "Creating directories and users" useradd -m -s /bin/bash $USER mkdir -p $TATAMI_DIR mkdir -p $TATAMI_DIR/application mkdir -p $TATAMI_DIR/maven mkdir -p $TATAMI_DIR/data mkdir -p $TATAMI_DIR/data/elasticsearch mkdir -p $TATAMI_DIR/log mkdir -p $TATAMI_DIR/log/elasticsearch ################################# ## Download Application ################################# echo "Getting the application from Github" cd $TATAMI_DIR/application git clone https://github.com/ippontech/tatami.git ################################# ## Install Cassandra ################################# cd $TATAMI_DIR echo "Installing JNA" sudo apt-get install libjna-java -y echo "Configuring OS limits" cp /etc/security/limits.conf /etc/security/limits.conf.original echo "* soft nofile 32768" | sudo tee -a /etc/security/limits.conf echo "* hard nofile 32768" | sudo tee -a /etc/security/limits.conf echo "root soft nofile 32768" | sudo tee -a /etc/security/limits.conf echo "root hard nofile 32768" | sudo tee -a /etc/security/limits.conf echo "* soft nofile 32768" >> /etc/security/limits.conf echo "* hard nofile 32768" >> /etc/security/limits.conf echo "root soft nofile 32768" >> /etc/security/limits.conf echo "root hard nofile 32768" >> /etc/security/limits.conf echo "* soft memlock unlimited" >> /etc/security/limits.conf echo "* hard memlock unlimited" >> /etc/security/limits.conf echo "root soft memlock unlimited" >> /etc/security/limits.conf echo "root hard memlock unlimited" >> /etc/security/limits.conf echo "* soft as unlimited" >> /etc/security/limits.conf echo "* hard as unlimited" >> /etc/security/limits.conf echo "root soft as unlimited" >> /etc/security/limits.conf echo "root hard as unlimited" >> /etc/security/limits.conf sysctl -w vm.max_map_count=131072 sudo swapoff --all # Cassandra Installation echo "Installing Cassandra" echo "deb http://debian.datastax.com/community stable main" >> /etc/apt/sources.list curl -L http://debian.datastax.com/debian/repo_key | sudo apt-key add - sudo apt-get update sudo apt-get install python-cql dsc1.1 -y sudo apt-get install opscenter-free -y sudo service opscenterd start ################################# ## Install Jetty ################################# echo "Installing Jetty" cd $TATAMI_DIR sysctl -w net.core.rmem_max=16777216 sysctl -w net.core.wmem_max=16777216 sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216" sysctl -w net.ipv4.tcp_wmem="4096 16384 16777216" sysctl -w net.core.somaxconn=4096 sysctl -w net.core.netdev_max_backlog=16384 sysctl -w net.ipv4.tcp_max_syn_backlog=8192 sysctl -w net.ipv4.tcp_syncookies=1 sysctl -w net.ipv4.ip_local_port_range="1024 65535" sysctl -w net.ipv4.tcp_tw_recycle=1 sysctl -w net.ipv4.tcp_congestion_control=cubic wget http://central.maven.org/maven2/org/mortbay/jetty/dist/jetty-deb/$JETTY_VERSION/jetty-deb-$JETTY_VERSION.deb dpkg -i jetty-deb-$JETTY_VERSION.deb rm -f jetty-deb-$JETTY_VERSION.deb rm -rf /opt/jetty/webapps/* ################################# ## Install Maven ################################# echo "Installing Maven" cd $TATAMI_DIR/maven wget http://mirrors.linsrv.net/apache/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz tar -xzf apache-maven-$MAVEN_VERSION-bin.tar.gz rm -f apache-maven-$MAVEN_VERSION-bin.tar.gz ln -s $TATAMI_DIR/maven/apache-maven-$MAVEN_VERSION $TATAMI_DIR/maven/current # Configure Maven for the tatami user echo "# Begin tatami configuration" >> /home/tatami/.profile echo "export M2_HOME=/opt/tatami/maven/current" >> /home/tatami/.profile echo "export PATH=/opt/tatami/maven/current/bin:$PATH" >> /home/tatami/.profile echo "export MAVEN_OPTS=\"-XX:MaxPermSize=64m -Xms256m -Xmx1024m\"" >> /home/tatami/.profile echo "# End tatami configuration" >> /home/tatami/.profile # Configure Maven repository mkdir -p $TATAMI_DIR/maven/repository cp $TATAMI_DIR/application/tatami/etc/installation/ubuntu/files/maven/settings.xml $TATAMI_DIR/maven/apache-maven-$MAVEN_VERSION/conf ################################# ## Install & run Application ################################# chown -R $USER $TATAMI_DIR ./update.sh ################################# ## Post install ################################# ================================================ FILE: etc/installation/ubuntu/uninstall.sh ================================================ #!/bin/sh # # description: Uninstalls Tatami on Ubuntu # This script must be run by the "root" user. # # Run this script directly by typing : # curl -L https://github.com/ippontech/tatami/raw/master/etc/installation/ubuntu/uninstall.sh | sudo bash # # - Deletes the "/opt/tatami" # - Deletes the "tatami" user echo "Tatami uninstaller" mv /etc/security/limits.conf.original /etc/security/limits.conf userdel -f -r tatami echo "Delete Tatami directory" rm -rf /opt/tatami ================================================ FILE: etc/installation/ubuntu/update.sh ================================================ #!/bin/sh # # description: Updates Tatami from Git # This script must be run by the "tatami" user, who must be a sudoer. echo "Welcome to the Tatami updater" ################################# # Variables ################################# echo "Setting up variables" export USER=tatami export TATAMI_DIR=/opt/tatami ################################# # Update application ################################# cd $TATAMI_DIR/application/tatami git pull cd /opt/tatami/application/tatami && mvn -Pprod -DskipTests clean package sudo /etc/init.d/jetty stop sudo cp /opt/tatami/application/tatami/target/root.war /opt/jetty/webapps/root.war sudo /etc/init.d/jetty start ================================================ FILE: jenkinsScripts/insertGoogleAuthKeys.sh ================================================ #!/bin/bash #Insert google auth variables into the build. googleKey=$1 googleSecret=$2 newServer=$3 usage='startTatami ' if [ "$#" -ne 3 ]; then echo "Need 3 paremeters" echo "$usage" exit 1 fi replaceKey='${tatami.google.clientId}' replaceSecret='${tatami.google.clientSecret}' oldServer="http:\/\/localhost:8080<\/tatami.url>" find ../src -name *security.xml | xargs sed -i "s/$replaceSecret/$googleSecret/g" find ../src -name *security.xml | xargs sed -i "s/$replaceKey/$googleKey/g" sed -i "s/$oldServer/$newServer/g" ../pom.xml ================================================ FILE: jenkinsScripts/restoreDatabase.sh ================================================ #!/bin/bash rm -rf ../target/cassandra || true rm -rf ./target/elasticsearch || true tar -zxvf save.tar.gz -C ../target/ tar -zxvf db.tar.gz -C ./target/ rm -f save.tar.gz || true ================================================ FILE: jenkinsScripts/saveDatabase.sh ================================================ #!/bin/bash tar -zcvf save.tar.gz ../target/cassandra ../target/elasticsearch tar -zcvf db.tar.gz ./target/elasticsearch ================================================ FILE: jenkinsScripts/startTatami.sh ================================================ #!/bin/bash #Insert google Authentication keys. ./insertGoogleAuthKeys.sh $1 $2 $3 if [ $? -ne 0 ]; then exit 1 fi #Start cassandra mvn -f ../pom.xml cassandra:run 2> cassandra.error >cassandra.log & echo "$!" >tatamiPID #start jetty mvn -f ../pom.xml jetty:run 2> jetty.error >jetty.log & echo "$!" >> tatamiPID ================================================ FILE: jenkinsScripts/stopTatami.sh ================================================ #!/bin/bash cat tatamiPID | xargs kill || true rm tatamiPID || true ================================================ FILE: mobile/.bowerrc ================================================ { "directory": "www/lib" } ================================================ FILE: mobile/.editorconfig ================================================ # http://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: mobile/.gitignore ================================================ # Specifies intentionally untracked files to ignore when using Git # http://git-scm.com/docs/gitignore www/lib/ node_modules/ platforms/ plugins/ ================================================ FILE: mobile/README.md ================================================ Tatami Mobile Beta ================== If interested in the mobile beta, follow these steps: Prepare Project --------------- - Clone, fork or download the source code from this Github page - Install [Maven](http://maven.apache.org/) - Install [Ionic](http://ionicframework.com/) - `npm install -g cordova ionic` - Point your terminal to the directory you cloned Tatami to. - `cd mobile` - Run Maven : `mvn -Pmobile-prod install` Deploy to Device ---------------- ### iOS - Open Xcode - Open `tatami/mobile/platforms/ios/Tatami.xcodeproj` - Connect device - Click play on top left ### Android - Connect device - Run the following command - `ionic build android && ionic run android` ================================================ FILE: mobile/bower.json ================================================ { "name": "tatami", "private": "true", "devDependencies": { "ionic": "~1.2.4", "angular-mocks": "1.4.3", "angular-marked": "~1.0.1", "angular-animate": "~1.5.3", "angular-sanitize": "~1.5.3", "angular-resource": "~1.5.3", "ngCordova": "~0.1.24-alpha", "angular-translate": "~2.11.0", "angular-translate-interpolation-messageformat": "~2.11.0", "angular-translate-loader-partial": "~2.11.0", "messageformat": "~0.3.1" }, "resolutions": { "angular-sanitize": "~1.5.3", "angular-animate": "~1.5.3", "ionic-toast": "v0.2.0" }, "dependencies": { "ionic-toast": "^0.4.1" } } ================================================ FILE: mobile/config.xml ================================================ Tatami The Tatami mobile app is designed to access the Tatami API, and it is an open source, enterprise social media platform. Ippon Technology, Inc ================================================ FILE: mobile/gulpfile.js ================================================ var gulp = require('gulp'); var gutil = require('gulp-util'); var bower = require('bower'); var concat = require('gulp-concat'); var sass = require('gulp-sass'); var minifyCss = require('gulp-minify-css'); var rename = require('gulp-rename'); var sh = require('shelljs'); var replace = require('replace'); var paths = { sass: ['./scss/**/*.scss'], css: ['./www/css/*.css'] }; gulp.task('default', ['sass', 'css']); gulp.task('sass', function(done) { gulp.src('./scss/ionic.app.scss') .pipe(sass()) .on('error', sass.logError) .pipe(gulp.dest('./www/css/')) .pipe(minifyCss({ keepSpecialComments: 0 })) .pipe(rename({ extname: '.min.css' })) .pipe(gulp.dest('./www/css/')) .on('end', done); }); gulp.task('css', function(done) { gulp.src('./www/css/style.css') .pipe(minifyCss({ keepSpecialComments: 0 })) .pipe(rename({ extname: '.min.css' })) .pipe(gulp.dest('./www/css/')) .on('end', done); }); gulp.task('watch', function() { gulp.watch(paths.sass, ['sass']); gulp.watch(paths.css, ['css']); }); gulp.task('install', ['git-check'], function() { return bower.commands.install() .on('log', function(data) { gutil.log('bower', gutil.colors.cyan(data.id), data.message); }); }); gulp.task('git-check', function(done) { if (!sh.which('git')) { console.log( ' ' + gutil.colors.red('Git is not installed.'), '\n Git, the version control system, is required to download Ionic.', '\n Download git here:', gutil.colors.cyan('http://git-scm.com/downloads') + '.', '\n Once git is installed, run \'' + gutil.colors.cyan('gulp install') + '\' again.' ); process.exit(1); } done(); }); var replaceFiles = ['./www/app/tatami.endpoint.js']; gulp.task('dev', function() { return replace({ regex: 'http://tatami.ippon.fr|http://10.1.10.202:8100', replacement: 'http://localhost:8100', paths: replaceFiles, recursive: false, silent: false }); }); gulp.task('device-dev', function() { return replace({ regex: 'http://localhost:8100|http://tatami.ippon.fr', replacement: 'http://10.1.10.202:8100', paths: replaceFiles, recursive: false, silent: false }); }); gulp.task('prod', function() { return replace({ regex: 'http://localhost:8100|http://10.1.10.202:8100', replacement: 'http://tatami.ippon.fr', paths: replaceFiles, recursive: false, silent: false }); }); ================================================ FILE: mobile/hooks/README.md ================================================ # Cordova Hooks This directory may contain scripts used to customize cordova commands. This directory used to exist at `.cordova/hooks`, but has now been moved to the project root. Any scripts you add to these directories will be executed before and after the commands corresponding to the directory name. Useful for integrating your own build systems or integrating with version control systems. __Remember__: Make your scripts executable. ## Hook Directories The following subdirectories will be used for hooks: after_build/ after_compile/ after_docs/ after_emulate/ after_platform_add/ after_platform_rm/ after_platform_ls/ after_plugin_add/ after_plugin_ls/ after_plugin_rm/ after_plugin_search/ after_prepare/ after_run/ after_serve/ before_build/ before_compile/ before_docs/ before_emulate/ before_platform_add/ before_platform_rm/ before_platform_ls/ before_plugin_add/ before_plugin_ls/ before_plugin_rm/ before_plugin_search/ before_prepare/ before_run/ before_serve/ pre_package/ <-- Windows 8 and Windows Phone only. ## Script Interface All scripts are run from the project's root directory and have the root directory passes as the first argument. All other options are passed to the script using environment variables: * CORDOVA_VERSION - The version of the Cordova-CLI. * CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). * CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) * CORDOVA_HOOK - Path to the hook that is being executed. * CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) If a script returns a non-zero exit code, then the parent cordova command will be aborted. ## Writing hooks We highly recommend writting your hooks using Node.js so that they are cross-platform. Some good examples are shown here: [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) ================================================ FILE: mobile/hooks/after_prepare/010_add_platform_class.js ================================================ #!/usr/bin/env node // Add Platform Class // v1.0 // Automatically adds the platform class to the body tag // after the `prepare` command. By placing the platform CSS classes // directly in the HTML built for the platform, it speeds up // rendering the correct layout/style for the specific platform // instead of waiting for the JS to figure out the correct classes. var fs = require('fs'); var path = require('path'); var rootdir = process.argv[2]; function addPlatformBodyTag(indexPath, platform) { // add the platform class to the body tag try { var platformClass = 'platform-' + platform; var cordovaClass = 'platform-cordova platform-webview'; var html = fs.readFileSync(indexPath, 'utf8'); var bodyTag = findBodyTag(html); if(!bodyTag) return; // no opening body tag, something's wrong if(bodyTag.indexOf(platformClass) > -1) return; // already added var newBodyTag = bodyTag; var classAttr = findClassAttr(bodyTag); if(classAttr) { // body tag has existing class attribute, add the classname var endingQuote = classAttr.substring(classAttr.length-1); var newClassAttr = classAttr.substring(0, classAttr.length-1); newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; newBodyTag = bodyTag.replace(classAttr, newClassAttr); } else { // add class attribute to the body tag newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); } html = html.replace(bodyTag, newBodyTag); fs.writeFileSync(indexPath, html, 'utf8'); process.stdout.write('add to body class: ' + platformClass + '\n'); } catch(e) { process.stdout.write(e); } } function findBodyTag(html) { // get the body tag try{ return html.match(/])(.*?)>/gi)[0]; }catch(e){} } function findClassAttr(bodyTag) { // get the body tag's class attribute try{ return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; }catch(e){} } if (rootdir) { // go through each of the platform directories that have been prepared var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); for(var x=0; x tatami fr.ippon.tatami 4.0.5 4.0.0 mobile fr.ippon.tatami services 4.0.5 fr.ippon.tatami services 4.0.5 test-jar test dev org.codehaus.mojo exec-maven-plugin 1.2.1 gulp install exec ${project.npm.bin}/gulp dev ionic install exec ionic serve -l device-dev org.codehaus.mojo exec-maven-plugin 1.2.1 install exec ${project.npm.bin}/gulp device-dev mobile-prod org.codehaus.mojo exec-maven-plugin 1.2.1 gulp install exec ${project.npm.bin}/gulp prod ionic install exec ionic build tatami-mobile-${project.version} maven-clean-plugin 2.6.1 ${project.basedir}/node_modules/ false org.codehaus.mojo exec-maven-plugin 1.2.1 install-npm-mobile-dependencies generate-sources npm install exec com.kelveden maven-karma-plugin 1.6 test start ${project.basedir}/node_modules/.bin/karma ${project.basedir}/karma.ci.conf.js ================================================ FILE: mobile/scss/ionic.app.scss ================================================ /* To customize the look and feel of Ionic, you can override the variables in ionic's _variables.scss file. For example, you might change some of the default colors: $light: #fff !default; $stable: #f8f8f8 !default; $positive: #387ef5 !default; $calm: #11c1f3 !default; $balanced: #33cd5f !default; $energized: #ffc900 !default; $assertive: #ef473a !default; $royal: #886aea !default; $dark: #444 !default; */ // The path for our ionicons font files, relative to the built CSS in www/css $ionicons-font-path: "../lib/ionic/release/fonts" !default; // Include all of Ionic @import "../www/lib/ionic/release/css/ionic"; .platform-ios { .tatami-location { @extend .ion-ios-location; } } .platform-android { .tatami-location { @extend .ion-android-locate; } } .item-image i:last-child { position: absolute; border-radius: 50%; width: 20px; height: 20px; background-color: black; top: 15px; right: 15px; } .link-span { cursor: pointer; color: #387ef5; } .center { margin-left: auto; margin-right: auto; display: block; } p ul { list-style-type: disc !important; list-style-position: inside !important; } //#list ul { // margin-top: 30px; //} //#list ul li { // text-align: left; // list-style: disc; // margin: 10px 0px; //} ================================================ FILE: mobile/www/app/components/follow/follow.html ================================================ ================================================ FILE: mobile/www/app/components/follow/follow.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(followConfig); followConfig.$inject = ['$stateProvider']; function followConfig($stateProvider) { $stateProvider .state('follow', { url: '/follow', parent: 'tatami', abstract: true, templateUrl: 'app/components/follow/follow.html', resolve: { currentUser: getCurrentUser, translatePartialLoader: getTranslatePartialLoader } }); getCurrentUser.$inject = ['ProfileService']; function getCurrentUser(ProfileService) { return ProfileService.get().$promise; } getTranslatePartialLoader.$inject = ['$translate', '$translatePartialLoader']; function getTranslatePartialLoader($translate, $translatePartialLoader) { $translatePartialLoader.addPart('follow'); return $translate.refresh(); } } })(); ================================================ FILE: mobile/www/app/components/follow/follower/follower.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('FollowerCtrl', followerCtrl); followerCtrl.$inject = ['followers', 'TatamiUserRefresherService', 'currentUser']; function followerCtrl(followers, TatamiUserRefresherService, currentUser) { var vm = this; vm.followers = followers; vm.getNewFollowers = getNewFollowers; function getNewFollowers() { TatamiUserRefresherService.refreshFollowers(currentUser).then(setUsers); } setUsers.$inject = ['followers']; function setUsers(followers) { vm.followers = followers; } } })(); ================================================ FILE: mobile/www/app/components/follow/follower/follower.html ================================================
================================================ FILE: mobile/www/app/components/follow/follower/follower.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('follower', { url: '/follower', parent: 'follow', views: { 'follower': { templateUrl: 'app/components/follow/follower/follower.html', controller: 'FollowerCtrl', controllerAs: 'vm' } }, resolve: { followers: followers } }); } followers.$inject = ['UserService', 'currentUser']; function followers(UserService, currentUser) { return UserService.getFollowers({ username: currentUser.username }).$promise; } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('follower', 'follow'); TatamiState.addConversationState('follower', 'follow'); TatamiState.addTagState('follower', 'follow'); } })(); ================================================ FILE: mobile/www/app/components/follow/following/following.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('FollowingCtrl', FollowingCtrl); FollowingCtrl.$inject = ['following', 'TatamiUserRefresherService', 'currentUser']; function FollowingCtrl(following, TatamiUserRefresherService, currentUser) { var vm = this; vm.following = following; vm.getNewFollowing = getNewFollowing; function getNewFollowing() { TatamiUserRefresherService.refreshFollowing(currentUser).then(setUsers); } setUsers.$inject = ['following']; function setUsers(following) { vm.following = following; } } })(); ================================================ FILE: mobile/www/app/components/follow/following/following.html ================================================
================================================ FILE: mobile/www/app/components/follow/following/following.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('following', { url: '/following', parent: 'follow', views: { 'following': { templateUrl: 'app/components/follow/following/following.html', controller: 'FollowingCtrl', controllerAs: 'vm' } }, resolve: { following: getFollowing } }); } getFollowing.$inject = ['UserService', 'currentUser']; function getFollowing(UserService, currentUser) { return UserService.getFollowing({ username: currentUser.username }).$promise; } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('following', 'follow'); TatamiState.addConversationState('following', 'follow'); TatamiState.addTagState('following', 'follow'); } })(); ================================================ FILE: mobile/www/app/components/follow/suggested/suggested.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('SuggestedCtrl', suggestedCtrl); suggestedCtrl.$inject = ['suggested', 'TatamiUserRefresherService']; function suggestedCtrl(suggested, TatamiUserRefresherService) { var vm = this; vm.suggested = suggested; vm.getNewSuggested = getNewSuggested; function getNewSuggested() { TatamiUserRefresherService.refreshSuggested().$promise.then(updateUsers); } updateUsers.$inject = ['suggested']; function updateUsers(suggested) { vm.suggested = suggested; } } })(); ================================================ FILE: mobile/www/app/components/follow/suggested/suggested.html ================================================
================================================ FILE: mobile/www/app/components/follow/suggested/suggested.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('suggested', { url: '/suggested', parent: 'follow', views: { 'suggested': { templateUrl: 'app/components/follow/suggested/suggested.html', controller: 'SuggestedCtrl', controllerAs: 'vm' } }, resolve: { suggested: getSuggested } }); } getSuggested.$inject = ['UserService']; function getSuggested(UserService) { return UserService.getSuggestions().$promise; } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('suggested', 'follow'); TatamiState.addConversationState('suggested', 'follow'); TatamiState.addTagState('suggested', 'follow'); } })(); ================================================ FILE: mobile/www/app/components/home/favorites/favorites.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('FavoritesCtrl', favoritesCtrl); favoritesCtrl.$inject = ['favorites', 'currentUser', 'TatamiStatusRefresherService', '$q']; function favoritesCtrl(favorites, currentUser, TatamiStatusRefresherService, $q) { var vm = this; vm.favorites = favorites; vm.currentUser = currentUser; vm.getNewStatuses = getNewStatuses; vm.getEmpty = getEmpty; function getNewStatuses() { return TatamiStatusRefresherService.refreshFavorites(); } getEmpty.$inject = ['finalStatus']; function getEmpty(finalStatus) { var deferred = $q.defer(); deferred.resolve([]); return deferred.promise; } } })(); ================================================ FILE: mobile/www/app/components/home/favorites/favorites.html ================================================ ================================================ FILE: mobile/www/app/components/home/favorites/favorites.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('favorites', { url: '/favorites', parent: 'home', views: { 'favorites': { templateUrl: 'app/components/home/favorites/favorites.html', controller: 'FavoritesCtrl', controllerAs: 'vm' } }, resolve: { favorites: favorites } }); favorites.$inject = ['HomeService']; function favorites(HomeService) { return HomeService.getFavorites().$promise; } } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('favorites', 'home'); TatamiState.addConversationState('favorites', 'home'); TatamiState.addTagState('favorites', 'home'); } })(); ================================================ FILE: mobile/www/app/components/home/home.html ================================================ ================================================ FILE: mobile/www/app/components/home/home.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(homeConfig); homeConfig.$inject = ['$stateProvider']; function homeConfig($stateProvider) { $stateProvider .state('home', { parent: 'tatami', abstract: true, url: '/home', templateUrl: 'app/components/home/home.html', resolve: { currentUser: getCurrentUser, translatePartialLoader: getTranslatePartialLoader } }) } getCurrentUser.$inject = ['ProfileService']; function getCurrentUser(ProfileService) { return ProfileService.get().$promise.then(function(currentUser) { console.log(currentUser); return currentUser; }); } getTranslatePartialLoader.$inject = ['$translate', '$translatePartialLoader']; function getTranslatePartialLoader($translate, $translatePartialLoader) { $translatePartialLoader.addPart('home'); return $translate.refresh(); } })(); ================================================ FILE: mobile/www/app/components/home/mentions/mentions.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('MentionsCtrl', mentionsCtrl); mentionsCtrl.$inject = ['mentioned', 'currentUser', 'TatamiStatusRefresherService']; function mentionsCtrl(mentioned, currentUser, TatamiStatusRefresherService) { var vm = this; vm.mentioned = mentioned; vm.currentUser = currentUser; vm.remove = remove; vm.getNewStatuses = getNewStatuses; vm.getOldStatuses = getOldStatuses; remove.$inject = ['mention']; function remove(mention) { vm.mentioned.splice(vm.mentioned.indexOf(mention), 1); } function getNewStatuses() { return TatamiStatusRefresherService.refreshMentions(); } getOldStatuses.$inject = ['finalStatus']; function getOldStatuses(finalStatus) { return TatamiStatusRefresherService.getOldMentions(finalStatus); } } })(); ================================================ FILE: mobile/www/app/components/home/mentions/mentions.html ================================================ ================================================ FILE: mobile/www/app/components/home/mentions/mentions.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('mentions', { url: '/mentions', parent: 'home', views: { 'mentions': { templateUrl: 'app/components/home/mentions/mentions.html', controller: 'MentionsCtrl', controllerAs: 'vm' } }, resolve: { mentioned: mentioned } }); } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('mentions', 'home'); TatamiState.addConversationState('mentions', 'home'); TatamiState.addTagState('mentions', 'home'); } mentioned.$inject = ['HomeService']; function mentioned(HomeService) { return HomeService.getMentions().$promise; } })(); ================================================ FILE: mobile/www/app/components/home/more/all_users/all.users.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('AllUsersController', allUsersController); allUsersController.$inject = ['currentUser', 'TatamiUserRefresherService', '$scope', '$timeout']; function allUsersController(currentUser, TatamiUserRefresherService, $scope, $timeout) { var vm = this; vm.currentUser = currentUser; vm.users = []; vm.isFinished = false; vm.getNextUsers = getNextUsers; function getNextUsers() { return TatamiUserRefresherService.getNextUsers(vm.users.length).then(addUsers); } addUsers.$inject = ['users']; function addUsers(nextUsers) { $timeout(function() { $scope.$apply(function() { vm.users.push.apply(vm.users, nextUsers); vm.isFinished = nextUsers.length === 0; }); }) } } })(); ================================================ FILE: mobile/www/app/components/home/more/all_users/all.users.html ================================================
================================================ FILE: mobile/www/app/components/home/more/all_users/all.users.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('allusers', { url: '/allusers', parent: 'more', views: { 'more@home': { templateUrl: 'app/components/home/more/all_users/all.users.html', controller: 'AllUsersController', controllerAs: 'vm' } } }); } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('allusers', 'home'); TatamiState.addConversationState('allusers', 'home'); TatamiState.addTagState('allusers', 'home'); } })(); ================================================ FILE: mobile/www/app/components/home/more/blocked_users/blocked.users.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('BlockedUsersController', blockedUsersController); blockedUsersController.$inject = ['$scope', 'currentUser', 'BlockService']; function blockedUsersController($scope, currentUser, BlockService) { var vm = this; vm.currentUser = currentUser; vm.blockedUsers = []; vm.updateUser = updateUser; vm.getBlockedUsersForUser = getBlockedUsersForUser; vm.hasBlockedUsers = hasBlockedUsers; function updateUser() { BlockService.updateBlockedUser( {username: vm.status.username} ); } function getBlockedUsersForUser() { BlockService.getBlockedUsersForUser( {username: vm.currentUser.username}, function (response) { vm.blockedUsers = response; } ); $scope.$broadcast('scroll.refreshComplete'); } function hasBlockedUsers() { return vm.blockedUsers.length>0; } } })(); ================================================ FILE: mobile/www/app/components/home/more/blocked_users/blocked.users.html ================================================
================================================ FILE: mobile/www/app/components/home/more/blocked_users/blocked.users.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('blockedusers', { url: '/blockedusers', parent: 'more', views: { 'more@home': { templateUrl: 'app/components/home/more/blocked_users/blocked.users.html', controller: 'BlockedUsersController', controllerAs: 'vm' } } }); } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('blockedusers', 'home'); TatamiState.addConversationState('blockedusers', 'home'); TatamiState.addTagState('blockedusers', 'home'); } })(); ================================================ FILE: mobile/www/app/components/home/more/company/company-timeline.html ================================================ ================================================ FILE: mobile/www/app/components/home/more/company/company.timeline.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('CompanyTimelineCtrl', companyTimelineCtrl); companyTimelineCtrl.$inject = ['statuses', 'currentUser', 'TatamiStatusRefresherService']; function companyTimelineCtrl(statuses, currentUser, TatamiStatusRefresherService) { var vm = this; vm.statuses = statuses; vm.currentUser = currentUser; vm.getNewStatuses = getNewStatuses; vm.getOldStatuses = getOldStatuses; function getNewStatuses() { return TatamiStatusRefresherService.refreshCompanyTimeline(); } getOldStatuses.$inject = ['finalStatus']; function getOldStatuses(finalStatus) { return TatamiStatusRefresherService.getOldFromCompanyTimeline(finalStatus); } } })(); ================================================ FILE: mobile/www/app/components/home/more/company/company.timeline.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('company', { url: '/company/timeline', parent: 'more', views: { 'more@home': { templateUrl: 'app/components/home/more/company/company-timeline.html', controller: 'CompanyTimelineCtrl', controllerAs: 'vm' } }, resolve: { statuses: getStatuses } }); getStatuses.$inject = ['HomeService']; function getStatuses(HomeService) { return HomeService.getCompanyTimeline().$promise; } } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('company', 'home'); TatamiState.addConversationState('company', 'home'); TatamiState.addTagState('company', 'home'); } })(); ================================================ FILE: mobile/www/app/components/home/more/more.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('MoreController', moreController); moreController.$inject = [ '$state', '$localStorage', 'currentUser' ]; function moreController($state, $localStorage, currentUser) { var vm = this; vm.currentUser = currentUser; vm.logout = logout; vm.goToCompanyTimeline = goToCompanyTimeline; vm.goToSettings = goToSettings; vm.goToBlockedUsers = goToBlockedUsers; vm.goToAllUsers = goToAllUsers; vm.goToReportedStatus = goToReportedStatus; function logout() { $localStorage.signOut(); $state.go('login'); } function goToCompanyTimeline() { $state.go('company'); } function goToSettings() { $state.go('settings'); } function goToBlockedUsers(){ $state.go('blockedusers'); } function goToReportedStatus(){ $state.go('reportedStatus') } function goToAllUsers() { $state.go('allusers') } } })(); ================================================ FILE: mobile/www/app/components/home/more/more.html ================================================
  Company Timeline All users   Settings Manage your blocked users Reported Statuses   Logout
================================================ FILE: mobile/www/app/components/home/more/more.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(moreConfig); moreConfig.$inject = ['$stateProvider']; function moreConfig($stateProvider) { $stateProvider .state('more', { url: '/more', parent: 'home', views: { 'more': { templateUrl: 'app/components/home/more/more.html', controller: 'MoreController', controllerAs: 'vm' } }, resolve: { translatePartialLoader: getTranslatePartialLoader } }); getTranslatePartialLoader.$inject = ['$translate', '$translatePartialLoader']; function getTranslatePartialLoader($translate, $translatePartialLoader) { $translatePartialLoader.addPart('more'); return $translate.refresh(); } } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('more', 'home'); } })(); ================================================ FILE: mobile/www/app/components/home/more/reportedStatus/reportedStatus.controller.js ================================================ /** * Created by emilyklein on 7/11/16. */ (function() { 'use strict'; angular.module('tatami') .controller('ReportedStatusController', reportedStatusController); reportedStatusController.$inject = ['$state', '$scope', 'currentUser', 'ReportService', '$ionicPopup', '$translate', 'ToastService']; function reportedStatusController($state, $scope, currentUser, ReportService, $ionicPopup, $translate, ToastService) { var vm = this; vm.reportedStatuses = []; vm.currentUser = currentUser; vm.getReportedStatuses = getReportedStatuses; vm.hasReportedStatus = hasReportedStatus; vm.approveStatus = approveStatus; vm.deleteStatus = deleteStatus; function reportStatus() { ReportService.reportStatus({statusId: vm.status.statusId}); $ionicPopup.alert({ title: 'Report', template: '' }); } goToProfile.$inject = ['username']; function goToProfile(username) { var destinationState = $state.current.name.split('.')[0] + '.profile'; $state.go(destinationState, { username : username }); } function getReportedStatuses(){ ReportService.getReportedStatuses(null, function(response){ vm.reportedStatuses = response; } ); $scope.$broadcast('scroll.refreshComplete'); } function deleteStatus(statusId){ var confirmPopup = $ionicPopup.confirm({ title: 'Delete Status', template: '' }); confirmPopup.then(deleted); deleted.$inject = ['decision']; function deleted(decision) { if(decision) { ReportService.deleteStatus({statusId: statusId}, function () { ToastService.display('status.reportStatus.deleteToast'); } ); $state.go($state.current, {}, {reload: true}); } } } function approveStatus(statusId){ var confirmPopup = $ionicPopup.confirm({ title: 'Approve Status', template: '' }); confirmPopup.then(approved); approved.$inject = ['decision']; function approved(decision) { if(decision) { ReportService.approveStatus({statusId: statusId}, function () { ToastService.display('status.reportStatus.approveToast'); } ); $state.go($state.current, {}, {reload: true}); } } } remove.$inject = ['status']; function remove(status) { vm.statuses.splice(vm.statuses.indexOf(status), 1); } function hasReportedStatus() { return vm.reportedStatuses.length > 0 } } })(); ================================================ FILE: mobile/www/app/components/home/more/reportedStatus/reportedStatus.html ================================================
================================================ FILE: mobile/www/app/components/home/more/reportedStatus/reportedStatus.js ================================================ /** * Created by emilyklein on 7/11/16. */ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('reportedStatus', { cache: false, url: '/reportedStatus', parent: 'more', views: { 'more@home': { templateUrl: 'app/components/home/more/reportedStatus/reportedStatus.html', controller: 'ReportedStatusController', controllerAs: 'vm' } } }); } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('reportedStatus', 'home'); TatamiState.addConversationState('reportedStatus', 'home'); TatamiState.addTagState('reportedStatus', 'home'); } })(); ================================================ FILE: mobile/www/app/components/home/more/settings/settings.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('SettingsController', settingsController); settingsController.$inject = ['$scope', '$translate', 'currentUser']; function settingsController($scope, $translate, currentUser) { var vm = this; vm.currentUser = currentUser; vm.language = window.localStorage.getItem('language'); vm.languages = [ { langKey: 'en', translateKey: 'more.language.english' }, { langKey: 'fr', translateKey: 'more.language.french' } ]; $scope.$watch('vm.language', updateLanguage); function updateLanguage(language) { $translate.use(language); window.localStorage.setItem('language', language); } } })(); ================================================ FILE: mobile/www/app/components/home/more/settings/settings.html ================================================ {{ language.translateKey | translate }} ================================================ FILE: mobile/www/app/components/home/more/settings/settings.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('settings', { url: '/settings', parent: 'more', views: { 'more@home': { templateUrl: 'app/components/home/more/settings/settings.html', controller: 'SettingsController', controllerAs: 'vm' } } }); } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('settings', 'home'); TatamiState.addConversationState('settings', 'home'); TatamiState.addTagState('settings', 'home'); } })(); ================================================ FILE: mobile/www/app/components/home/timeline/timeline.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('TimelineCtrl', timelineCtrl); timelineCtrl.$inject = ['statuses', 'currentUser', 'TatamiStatusRefresherService']; function timelineCtrl(statuses, currentUser, TatamiStatusRefresherService) { var vm = this; vm.statuses = statuses; vm.currentUser = currentUser; vm.getNewStatuses = getNewStatuses; vm.getOldStatuses = getOldStatuses; function getNewStatuses() { return TatamiStatusRefresherService.refreshHomeTimeline(); } getOldStatuses.$inject = ['finalStatus']; function getOldStatuses(finalStatus) { return TatamiStatusRefresherService.getOldFromHomeTimeline(finalStatus); } } })(); ================================================ FILE: mobile/www/app/components/home/timeline/timeline.html ================================================ ================================================ FILE: mobile/www/app/components/home/timeline/timeline.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('timeline', { url: '/timeline', parent: 'home', views: { 'timeline': { templateUrl: 'app/components/home/timeline/timeline.html', controller: 'TimelineCtrl', controllerAs: 'vm' } }, resolve: { statuses: getStatuses } }); getStatuses.$inject = ['StatusService']; function getStatuses(StatusService) { return StatusService.getHomeTimeline().$promise; } } angular.module('tatami') .run(run); run.$inject = ['TatamiState']; function run(TatamiState) { TatamiState.addProfileState('timeline', 'home'); TatamiState.addConversationState('timeline', 'home'); TatamiState.addTagState('timeline', 'home'); } })(); ================================================ FILE: mobile/www/app/components/login/login.controller.js ================================================ (function () { 'use strict'; angular.module('tatami') .controller('LoginCtrl', loginCtrl); loginCtrl.$inject = [ 'TatamiEndpoint', '$scope', '$state', '$http', '$localStorage', '$ionicLoading', 'PathService', '$ionicHistory', 'ToastService' ]; function loginCtrl(TatamiEndpoint, $scope, $state, $http, $localStorage, $ionicLoading, PathService, $ionicHistory, ToastService) { var vm = this; $scope.$on("$ionicView.enter", function () { $ionicHistory.clearCache(); $ionicHistory.clearHistory(); var newEndpoint = TatamiEndpoint.getEndpoint(); if (vm.lastEndpoint.url !== newEndpoint.url) { vm.lastEndpoint.url = newEndpoint.url; vm.failed = false; } }); vm.user = { remember: false }; vm.failed = false; vm.login = login; vm.tryGoogleLogin = tryGoogleLogin; vm.goToServerConfig = goToServerConfig; vm.lastEndpoint = {url: ''}; function login() { var data = "j_username=" + encodeURIComponent(vm.user.email) + "&j_password=" + encodeURIComponent(vm.user.password); return $http.post('/tatami/rest/authentication', data, { headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" } }).success(function (data) { $localStorage.set('token', data.token); vm.user = {remember: false}; $state.go('timeline'); }).error(function () { vm.failed = true; }); } function tryGoogleLogin() { $http({ url: '/tatami/rest/client/id', method: 'GET' }).then(function (data) { var clientId; if (data && data.data && data.data.stringList) { // Old tatami return the clientId in the stringList property clientId = data.data.stringList[0]; } else if (data && data.data && data.data.clientId) { clientId = data.data.clientId; } // Do Google login or display an error message if (clientId) { googleLogin(clientId); } else { ToastService.display('login.googleUnavaible'); } }, function () { ToastService.display('login.googleUnavaible'); }); } function googleLogin(clientId) { var emailScope = 'https://www.googleapis.com/auth/plus.profile.emails.read'; var profileScope = 'https://www.googleapis.com/auth/plus.me'; var googleUrl = 'https://accounts.google.com/o/oauth2/auth?' + 'client_id=' + clientId + '&' + 'redirect_uri=http://localhost/callback&' + 'scope=' + emailScope + ' ' + profileScope + '&' + 'approval_prompt=force&response_type=code&access_type=offline'; var ref = window.open(googleUrl, '_blank', 'location=no'); ref.addEventListener('loadstart', onStart); onStart.$inject = ['event']; function onStart(event) { if (event.url.indexOf('http://localhost/callback') === 0) { ref.close(); $ionicLoading.show({ template: 'Login in progress...', hideOnStateChange: true }); var requestToken = event.url.split("code=")[1]; $http({ url: '/tatami/rest/oauth/token', method: 'POST', headers: { 'x-auth-code-header': requestToken } }).then(onSuccess, onFail); } } onSuccess.$inject = ['result']; function onSuccess(result) { $localStorage.set('token', result.data.token); $state.go('timeline'); } onFail.$inject = ['failure']; function onFail() { vm.failed = true; } } function goToServerConfig() { $state.go('server'); } } })(); ================================================ FILE: mobile/www/app/components/login/login.html ================================================

Failed to log in!

================================================ FILE: mobile/www/app/components/login/login.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('login', { url: '/login', parent: 'tatami', templateUrl: 'app/components/login/login.html', controller: 'LoginCtrl', controllerAs: 'vm', resolve: { translatePartialLoader: getTranslatePartialLoader } }); getTranslatePartialLoader.$inject = ['$translate', '$translatePartialLoader']; function getTranslatePartialLoader($translate, $translatePartialLoader) { $translatePartialLoader.addPart('login'); return $translate.refresh(); } } })(); ================================================ FILE: mobile/www/app/components/login/server/server.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('ServerController', serverController); serverController.$inject = [ '$state', '$http', '$ionicHistory', '$translate', '$ionicPopup', 'PathService', 'TatamiEndpoint' ]; function serverController($state, $http, $ionicHistory, $translate, $ionicPopup, PathService, TatamiEndpoint) { var vm = this; vm.endpoint = TatamiEndpoint.getEndpoint().url || TatamiEndpoint.getDefault().url; vm.success = true; vm.previous = vm.endpoint; vm.updateEndpoint = updateEndpoint; vm.useDefaultEndpoint = useDefaultEndpoint; vm.isLastAttempt = isLastAttempt; function updateEndpoint() { TatamiEndpoint.setEndpoint(vm.endpoint); $http({ url: '/tatami/rest/client/id', method: 'GET' }).then(success, error); } function success(result) { vm.success = true; vm.previous = vm.endpoint; var alertPopup = $ionicPopup.alert({ title: $translate.instant('server.endpoint.authenticate.title'), template: '' }); alertPopup.then($state.go('login')); } function error(result) { vm.success = false; vm.previous = vm.endpoint; $ionicPopup.alert({ title: $translate.instant('server.endpoint.error.title'), template: '' }); TatamiEndpoint.reset(); } function isLastAttempt() { return vm.previous === vm.endpoint; } function useDefaultEndpoint() { vm.endpoint = TatamiEndpoint.getDefault().url; updateEndpoint(); } } })(); ================================================ FILE: mobile/www/app/components/login/server/server.html ================================================ ================================================ FILE: mobile/www/app/components/login/server/server.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(config); config.$inject = ['$stateProvider']; function config($stateProvider) { $stateProvider .state('server', { url: '/server', parent: 'login', views: { '@tatami': { templateUrl: 'app/components/login/server/server.html', controller: 'ServerController', controllerAs: 'vm' } }, resolve: { translatePartialLoader: getTranslatePartialLoader } }); getTranslatePartialLoader.$inject = ['$translate', '$translatePartialLoader']; function getTranslatePartialLoader($translate, $translatePartialLoader) { $translatePartialLoader.addPart('server'); $translate.refresh(); } } })(); ================================================ FILE: mobile/www/app/components/post/post.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('PostCtrl', postCtrl); postCtrl.$inject = [ 'StatusService', 'PathService', '$ionicHistory', '$state', '$cordovaCamera', '$q', '$ionicLoading', '$ionicPopup', 'repliedToStatus', '$scope', '$cordovaGeolocation', 'ToastService' ]; function postCtrl(StatusService, PathService, $ionicHistory, $state, $cordovaCamera, $q, $ionicLoading, $ionicPopup, repliedToStatus, $scope, $cordovaGeolocation, ToastService) { var vm = this; vm.charCount = 750; vm.status = { content: repliedToStatus ? '@' + repliedToStatus.username : '', statusPrivate: repliedToStatus ? repliedToStatus.private : false, replyTo: repliedToStatus ? repliedToStatus.statusId : '', replyToUsername: repliedToStatus ? repliedToStatus.username : '', attachmentIds: [], geoLocalization: "" }; vm.images = []; vm.isPosting = false; vm.remainingLength = vm.charCount; vm.newLineCount = 0; //This field takes into consideration the '\n' character that counts for 2 chars in the database. vm.pasteFlag = false; vm.shareLocation = false; vm.post = post; vm.reset = reset; vm.close = close; vm.getPicture = getPicture; vm.getPictureFromLibrary = getPictureFromLibrary; vm.remove = remove; vm.paste = paste; vm.updateLocation = updateLocation; vm.updatePrivate = updatePrivate; function post() { upload().then(createPost); } function createPost(attachmentIds) { vm.status.attachmentIds = attachmentIds; StatusService.save(vm.status, function() { reset(); $ionicHistory.clearCache(); $state.go('timeline'); }); } function reset() { vm.status = { content: '', statusPrivate: false, attachmentIds: [], geoLocalization: "" }; vm.images = []; vm.isPosting = false; vm.shareLocation = false; } function close() { $ionicHistory.goBack(); reset(); } function getPicture() { var options = { quality: 10, correctOrientation : true }; $cordovaCamera.getPicture(options).then(store); } function getPictureFromLibrary() { var options = { quality: 10, sourceType: Camera.PictureSourceType.PHOTOLIBRARY, correctOrientation: true }; $cordovaCamera.getPicture(options).then(store); } function store(fileUri) { vm.images.push(fileUri); } function upload() { var promises = []; vm.isPosting = true; var options = new FileUploadOptions(); var fileTransfer = new FileTransfer(); angular.forEach(vm.images, function(image) { var deferred = $q.defer(); options.fileKey = 'uploadFile'; options.fileName = image.substr(image.lastIndexOf('/') + 1); $ionicLoading.show({ template: 'Post in progress...', hideOnStateChange: true }); fileTransfer.upload(image, '/tatami/rest/fileupload', onSuccess, onFail, options); promises.push(deferred.promise); function onSuccess(result) { var jsonResult = JSON.parse(result.response)[0]; deferred.resolve(jsonResult.attachmentId); } function onFail(failure) { $ionicLoading.hide(); var popupError = $ionicPopup.alert({ title: 'Error', template: '' }); popupError.then(goToTimeline); function goToTimeline() { reset(); $state.go('timeline'); } deferred.resolve(failure); } }); return $q.all(promises); } function remove(index) { vm.images.splice(index, 1); } function updateRemainingLength(statusContent) { vm.remainingLength = vm.charCount - statusContent.length - vm.newLineCount; } function updateNewLineCount(statusContent) { vm.newLineCount = (statusContent.match(/\n/g) || []).length; } $scope.$watch('vm.status.content', function (newValue) { if (newValue) { updateNewLineCount(newValue); updateRemainingLength(newValue); if(vm.remainingLength < 0){ if(vm.pasteFlag){ $ionicLoading.show({ template: '', duration: 2500 }); vm.pasteFlag = false; } vm.status.content = newValue.slice(0, vm.charCount - vm.newLineCount); updateNewLineCount(vm.status.content); updateRemainingLength(vm.status.content); } } else { vm.remainingLength = vm.charCount; vm.newLineCount = 0; } }); function paste(){ vm.pasteFlag = true; } function updateLocation() { vm.shareLocation = !vm.shareLocation; if(vm.shareLocation){ var posOptions = {timeout: 5000, enableHighAccuracy: false}; $cordovaGeolocation .getCurrentPosition(posOptions) .then(function (position) { vm.status.geoLocalization = position.coords.latitude + ", " + position.coords.longitude; ToastService.display('post.location.share'); }, function() { vm.shareLocation = !vm.shareLocation; ToastService.display('post.location.fail'); }); } else { vm.status.geoLocalization = ""; } } function updatePrivate(){ vm.status.statusPrivate = !vm.status.statusPrivate; vm.status.statusPrivate ? ToastService.display('post.private.yes') : ToastService.display('post.private.no'); } } })(); ================================================ FILE: mobile/www/app/components/post/post.html ================================================

{{vm.remainingLength}}

================================================ FILE: mobile/www/app/components/post/post.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(postConfig); postConfig.$inject = ['$stateProvider']; function postConfig($stateProvider) { $stateProvider .state('post', { url: '/post/:statusId', parent: 'tatami', templateUrl: 'app/components/post/post.html', controller: 'PostCtrl', controllerAs: 'vm', resolve: { repliedToStatus: getRepliedToStatus, translatePartialProvider: getTranslatePartialLoader } }) } getRepliedToStatus.$inject = ['StatusService', '$stateParams']; function getRepliedToStatus(StatusService, $stateParams) { if($stateParams.statusId) { return StatusService.get({ statusId: $stateParams.statusId }).$promise; } } getTranslatePartialLoader.$inject = ['$translate', '$translatePartialLoader']; function getTranslatePartialLoader($translate, $translatePartialLoader) { $translatePartialLoader.addPart('post'); return $translate.refresh(); } })(); ================================================ FILE: mobile/www/app/components/post/postbar.directive.js ================================================ (function() { 'use strict'; angular.module('tatami') .directive('tatamiPostBarAttach', tatamiPostBar); function tatamiPostBar() { var directive = { restrict: 'A', link: link }; return directive; } function link(scope, element, attrs) { ionic.on('native.keyboardshow', onShow, window); ionic.on('native.keyboardhide', onHide, window); //deprecated ionic.on('native.showkeyboard', onShow, window); ionic.on('native.hidekeyboard', onHide, window); var scrollCtrl; function onShow(e) { if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) { return; } if(attrs['tatamiPostBarAttach'] === 'true') { var subheaderOffset = 43; } //for testing var keyboardHeight = e.keyboardHeight || e.detail.keyboardHeight; element.css('bottom', (keyboardHeight + subheaderOffset) + "px"); scrollCtrl = element.controller('$ionicScroll'); if (scrollCtrl) { scrollCtrl.scrollView.__container.style.bottom = subheaderOffset + keyboardHeight + keyboardAttachGetClientHeight(element[0]) + "px"; } } function onHide() { if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) { return; } element.css('bottom', ''); if (scrollCtrl) { scrollCtrl.scrollView.__container.style.bottom = ''; } } scope.$on('$destroy', function() { ionic.off('native.keyboardshow', onShow, window); ionic.off('native.keyboardhide', onHide, window); //deprecated ionic.off('native.showkeyboard', onShow, window); ionic.off('native.hidekeyboard', onHide, window); }); function keyboardAttachGetClientHeight(element) { return element.clientHeight; } } })(); ================================================ FILE: mobile/www/app/shared/config/marked.config.js ================================================ (function() { 'use strict'; angular.module('tatami') .config(markedConfig); markedConfig.$inject = ['markedProvider']; function markedConfig(markedProvider) { markedProvider.setOptions({ gfm: true, pedantic: false, sanitize: true, highlight: null, urls: { youtube : function(text, url){ var cap; if((cap = /(youtu\.be\/|youtube\.com\/(watch\?(.*&)?v=|(embed|v)\/))([^\?&"'>]+)/.exec(url))){ return ''; } }, vimeo : function(text, url){ var cap; if((cap = /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/.exec(url))){ return ''; } }, dailymotion : function(text, url){ var cap; if((cap = /^.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/.exec(url))){ return ''; } }, gist : function(text, url){ var cap; if((cap = /^.+gist.github.com\/(([A-z0-9-]+)\/)?([0-9A-z]+)/.exec(url))){ $.ajax({ url: cap[0] + '.json', dataType: 'jsonp', success: function(response){ if(response.stylesheet && $('link[href="' + response.stylesheet + '"]').length === 0){ var l = document.createElement("link"), head = document.getElementsByTagName("head")[0]; l.type = "text/css"; l.rel = "stylesheet"; l.href = response.stylesheet; head.insertBefore(l, head.firstChild); } var $elements = $('.gist' + cap[3]); $elements.html(response.div); } }); return '
'; } } } }); } })(); ================================================ FILE: mobile/www/app/shared/config/marked.filter.js ================================================ (function() { 'use strict'; angular.module('tatami') .filter('markdown', markdown); markdown.$inject = ['$sce']; function markdown($sce) { return function(content) { return content ? marked(content) : ''; }; } })(); ================================================ FILE: mobile/www/app/shared/config/tatami.marked.js ================================================ /** * marked - a markdown parser * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed) * https://github.com/chjj/marked */ ;(function() { /** * Block-Level Grammar */ var block = { newline: /^\n+/, code: /^( {4}[^\n]+\n*)+/, fences: noop, hr: /^( *[-*_]){3,} *(?:\n+|$)/, heading: /^ *(#{1,6} ) *([^\n]+?) *#* *(?:\n+|$)/, nptable: noop, lheading: /^([^\n]+)\n *(=|-){3,} *\n*/, blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/, list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/, def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, table: noop, paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, text: /^[^\n]+/ }; block.bullet = /(?:[*+-]|\d+\.)/; block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; block.item = replace(block.item, 'gm') (/bull/g, block.bullet) (); block.list = replace(block.list) (/bull/g, block.bullet) ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/) (); block._tag = '(?!(?:' + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b'; block.html = replace(block.html) ('comment', //) ('closed', /<(tag)[\s\S]+?<\/\1>/) ('closing', /])*?>/) (/tag/g, block._tag) (); block.paragraph = replace(block.paragraph) ('hr', block.hr) ('heading', block.heading) ('lheading', block.lheading) ('blockquote', block.blockquote) ('tag', '<' + block._tag) ('def', block.def) (); /** * Normal Block Grammar */ block.normal = merge({}, block); /** * GFM Block Grammar */ block.gfm = merge({}, block.normal, { fences: /^ *(`{3,}|~{3,}) *(\w+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, paragraph: /^/ }); block.gfm.paragraph = replace(block.paragraph) ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|') (); /** * GFM + Tables Block Grammar */ block.tables = merge({}, block.gfm, { nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ }); /** * Block Lexer */ function Lexer(options) { this.tokens = []; this.tokens.links = {}; this.options = options || marked.defaults; this.rules = block.normal; if (this.options.gfm) { if (this.options.tables) { this.rules = block.tables; } else { this.rules = block.gfm; } } } /** * Expose Block Rules */ Lexer.rules = block; /** * Static Lex Method */ Lexer.lex = function(src, options) { var lexer = new Lexer(options); return lexer.lex(src); }; /** * Preprocessing */ Lexer.prototype.lex = function(src) { src = src .replace(/\r\n|\r/g, '\n') .replace(/\t/g, ' ') .replace(/\u00a0/g, ' ') .replace(/\u2424/g, '\n'); return this.token(src, true); }; /** * Lexing */ Lexer.prototype.token = function(src, top) { var src = src.replace(/^ +$/gm, '') , next , loose , cap , bull , b , item , space , i , l; while (src) { // newline if (cap = this.rules.newline.exec(src)) { src = src.substring(cap[0].length); if (cap[0].length > 1) { this.tokens.push({ type: 'space' }); } } // code if (cap = this.rules.code.exec(src)) { src = src.substring(cap[0].length); cap = cap[0].replace(/^ {4}/gm, ''); this.tokens.push({ type: 'code', text: !this.options.pedantic ? cap.replace(/\n+$/, '') : cap }); continue; } // fences (gfm) if (cap = this.rules.fences.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'code', lang: cap[2], text: cap[3] }); continue; } // heading if (cap = this.rules.heading.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'heading', depth: cap[1].length, text: cap[2] }); continue; } // table no leading pipe (gfm) if (top && (cap = this.rules.nptable.exec(src))) { src = src.substring(cap[0].length); item = { type: 'table', header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), cells: cap[3].replace(/\n$/, '').split('\n') }; for (i = 0; i < item.align.length; i++) { if (/^ *-+: *$/.test(item.align[i])) { item.align[i] = 'right'; } else if (/^ *:-+: *$/.test(item.align[i])) { item.align[i] = 'center'; } else if (/^ *:-+ *$/.test(item.align[i])) { item.align[i] = 'left'; } else { item.align[i] = null; } } for (i = 0; i < item.cells.length; i++) { item.cells[i] = item.cells[i].split(/ *\| */); } this.tokens.push(item); continue; } // lheading if (cap = this.rules.lheading.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'heading', depth: cap[2] === '=' ? 1 : 2, text: cap[1] }); continue; } // hr if (cap = this.rules.hr.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'hr' }); continue; } // blockquote if (cap = this.rules.blockquote.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'blockquote_start' }); cap = cap[0].replace(/^ *> ?/gm, ''); // Pass `top` to keep the current // "toplevel" state. This is exactly // how markdown.pl works. this.token(cap, top); this.tokens.push({ type: 'blockquote_end' }); continue; } // list if (cap = this.rules.list.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'list_start', ordered: isFinite(cap[2]) }); // Get each top-level item. cap = cap[0].match(this.rules.item); // Get bullet. if (this.options.smartLists) { bull = block.bullet.exec(cap[0])[0]; } next = false; l = cap.length; i = 0; for (; i < l; i++) { item = cap[i]; // Remove the list item's bullet // so it is seen as the next token. space = item.length; item = item.replace(/^ *([*+-]|\d+\.) +/, ''); // Outdent whatever the // list item contains. Hacky. if (~item.indexOf('\n ')) { space -= item.length; item = !this.options.pedantic ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') : item.replace(/^ {1,4}/gm, ''); } // Determine whether the next list item belongs here. // Backpedal if it does not belong in this list. if (this.options.smartLists && i !== l - 1) { b = block.bullet.exec(cap[i+1])[0]; if (bull !== b && !(bull[1] === '.' && b[1] === '.')) { src = cap.slice(i + 1).join('\n') + src; i = l - 1; } } // Determine whether item is loose or not. // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ // for discount behavior. loose = next || /\n\n(?!\s*$)/.test(item); if (i !== l - 1) { next = item[item.length-1] === '\n'; if (!loose) loose = next; } this.tokens.push({ type: loose ? 'loose_item_start' : 'list_item_start' }); // Recurse. this.token(item, false); this.tokens.push({ type: 'list_item_end' }); } this.tokens.push({ type: 'list_end' }); continue; } // html if (cap = this.rules.html.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: this.options.sanitize ? 'paragraph' : 'html', pre: cap[1] === 'pre', text: cap[0] }); continue; } // def if (top && (cap = this.rules.def.exec(src))) { src = src.substring(cap[0].length); this.tokens.links[cap[1].toLowerCase()] = { href: cap[2], title: cap[3] }; continue; } // table (gfm) if (top && (cap = this.rules.table.exec(src))) { src = src.substring(cap[0].length); item = { type: 'table', header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') }; for (i = 0; i < item.align.length; i++) { if (/^ *-+: *$/.test(item.align[i])) { item.align[i] = 'right'; } else if (/^ *:-+: *$/.test(item.align[i])) { item.align[i] = 'center'; } else if (/^ *:-+ *$/.test(item.align[i])) { item.align[i] = 'left'; } else { item.align[i] = null; } } for (i = 0; i < item.cells.length; i++) { item.cells[i] = item.cells[i] .replace(/^ *\| *| *\| *$/g, '') .split(/ *\| */); } this.tokens.push(item); continue; } // top-level paragraph if (top && (cap = this.rules.paragraph.exec(src))) { src = src.substring(cap[0].length); this.tokens.push({ type: 'paragraph', text: cap[1][cap[1].length-1] === '\n' ? cap[1].slice(0, -1) : cap[1] }); continue; } // text if (cap = this.rules.text.exec(src)) { // Top-level should never reach here. src = src.substring(cap[0].length); this.tokens.push({ type: 'text', text: cap[0] }); continue; } if (src) { throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); } } return this.tokens; }; /** * Inline-Level Grammar */ var inline = { escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, url: noop, tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, link: /^!?\[(inside)\]\(href\)/, reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, br: /^ {2,}\n(?!\s*$)/, mail: /^([^\s !"#$%'()*+,.\/:;<=>?@\\\[\]\^_`{|}~-]+(@|:\/)[^\s !"#$%'()*+,.\/:;<=>?@\\\[\]\^_`{|}~-]+.[^\s !"#$%'()*+,.\/:;<=>?@\\\[\]\^_`{|}~-]+)/, mention: /^@([A-Za-z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[A-Za-z0-9!#$%&'*+\/=?\^_`{|}~\-]+)*(?!\*))/, tags: /^#([^\s!"#$%'()*+,.\/:;<=>?@\\\[\]\^_`{|}~-]+(?:\.[\^!"#$%'()*+,.\/:;<=>?@\\\[\]\^_`{|}~-]+)*)(?!\*)/, del: noop, text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; inline.link = replace(inline.link) ('inside', inline._inside) ('href', inline._href) (); inline.reflink = replace(inline.reflink) ('inside', inline._inside) (); /** * Normal Inline Grammar */ inline.normal = merge({}, inline); /** * Pedantic Inline Grammar */ inline.pedantic = merge({}, inline.normal, { strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ }); /** * GFM Inline Grammar */ inline.gfm = merge({}, inline.normal, { escape: replace(inline.escape)('])', '~|])')(), url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, del: /^~~(?=\S)([\s\S]*?\S)~~/, text: replace(inline.text) (']|', '#@~]|') ('|', '|https?://|') ('|', '||') () }); /** * GFM + Line Breaks Inline Grammar */ inline.breaks = merge({}, inline.gfm, { br: replace(inline.br)('{2,}', '*')(), text: replace(inline.gfm.text)('{2,}', '*')() }); /** * Inline Lexer & Compiler */ function InlineLexer(links, options) { this.options = options || marked.defaults; this.links = links; this.rules = inline.normal; if (!this.links) { throw new Error('Tokens array requires a `links` property.'); } if (this.options.gfm) { if (this.options.breaks) { this.rules = inline.breaks; } else { this.rules = inline.gfm; } } else if (this.options.pedantic) { this.rules = inline.pedantic; } } /** * Expose Inline Rules */ InlineLexer.rules = inline; /** * Static Lexing/Compiling Method */ InlineLexer.output = function(src, links, options) { var inline = new InlineLexer(links, options); return inline.output(src); }; /** * Lexing/Compiling */ InlineLexer.prototype.output = function(src) { var out = '' , link , text , href , cap; while (src) { // escape if (cap = this.rules.escape.exec(src)) { src = src.substring(cap[0].length); out += cap[1]; continue; } // autolink if (cap = this.rules.autolink.exec(src)) { src = src.substring(cap[0].length); if (cap[2] === '@') { text = cap[1][6] === ':' ? this.mangle(cap[1].substring(7)) : this.mangle(cap[1]); href = this.mangle('mailto:') + text; } else { href = escape(cap[1]); text = shortenUrl(href); } out += '' + text + ''; continue; } // autolink if (cap = this.rules.mail.exec(src)) { src = src.substring(cap[0].length); if (cap[2] === '@') { text = cap[1][6] === ':' ? this.mangle(cap[1].substring(7)) : this.mangle(cap[1]); href = this.mangle('mailto:') + text; } else { href = escape(cap[1]); text = shortenUrl(href); } out += '' + text + ''; continue; } // url (gfm) if (cap = this.rules.url.exec(src)) { var html; src = src.substring(cap[0].length); href = escape(cap[1]); text = shortenUrl(href); for(var key in this.options.urls){ html = this.options.urls[key](text, href); if(html){ out += html; break; } } if(!html) out += '' + text + ''; continue; } // tag if (cap = this.rules.tag.exec(src)) { src = src.substring(cap[0].length); out += this.options.sanitize ? escape(cap[0]) : cap[0]; continue; } // link if (cap = this.rules.link.exec(src)) { src = src.substring(cap[0].length); out += this.outputLink(cap, { href: cap[2], title: cap[3] }); continue; } // reflink, nolink if ((cap = this.rules.reflink.exec(src)) || (cap = this.rules.nolink.exec(src))) { src = src.substring(cap[0].length); link = (cap[2] || cap[1]).replace(/\s+/g, ' '); link = this.links[link.toLowerCase()]; if (!link || !link.href) { out += cap[0][0]; src = cap[0].substring(1) + src; continue; } out += this.outputLink(cap, link); continue; } // strong if (cap = this.rules.strong.exec(src)) { src = src.substring(cap[0].length); out += '' + this.output(cap[2] || cap[1]) + ''; continue; } // em if (cap = this.rules.em.exec(src)) { src = src.substring(cap[0].length); out += '' + this.output(cap[2] || cap[1]) + ''; continue; } // code if (cap = this.rules.code.exec(src)) { src = src.substring(cap[0].length); out += '' + escape(cap[2], true) + ''; continue; } // mention if (cap = inline.mention.exec(src)) { src = src.substring(cap[0].length); out += '' + cap[0] + ''; continue; } // tags if (cap = inline.tags.exec(src)) { src = src.substring(cap[0].length); out += '' + cap[0] + ''; continue; } // br if (cap = this.rules.br.exec(src)) { src = src.substring(cap[0].length); out += '
'; continue; } // del (gfm) if (cap = this.rules.del.exec(src)) { src = src.substring(cap[0].length); out += '' + this.output(cap[1]) + ''; continue; } // text if (cap = this.rules.text.exec(src)) { src = src.substring(cap[0].length); out += escape(cap[0]); continue; } if (src) { throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); } } return out; }; /** * Compile Link */ InlineLexer.prototype.outputLink = function(cap, link) { if (cap[0][0] !== '!') { return '' + this.output(cap[1]) + ''; } else { return ''
                + escape(cap[1])
                + ''; } }; /** * Mangle Links */ InlineLexer.prototype.mangle = function(text) { var out = '' , l = text.length , i = 0 , ch; for (; i < l; i++) { ch = text.charCodeAt(i); if (Math.random() > 0.5) { ch = 'x' + ch.toString(16); } out += '&#' + ch + ';'; } return out; }; /** * Parsing & Compiling */ function Parser(options) { this.tokens = []; this.token = null; this.options = options || marked.defaults; } /** * Static Parse Method */ Parser.parse = function(src, options) { var parser = new Parser(options); return parser.parse(src); }; /** * Parse Loop */ Parser.prototype.parse = function(src) { this.inline = new InlineLexer(src.links, this.options); this.tokens = src.reverse(); var out = ''; while (this.next()) { out += this.tok(); } return out; }; /** * Next Token */ Parser.prototype.next = function() { return this.token = this.tokens.pop(); }; /** * Preview Next Token */ Parser.prototype.peek = function() { return this.tokens[this.tokens.length-1] || 0; }; /** * Parse Text Tokens */ Parser.prototype.parseText = function() { var body = this.token.text; while (this.peek().type === 'text') { body += '\n' + this.next().text; } return this.inline.output(body); }; /** * Parse Current Token */ Parser.prototype.tok = function() { switch (this.token.type) { case 'space': { return ''; } case 'hr': { return '
\n'; } case 'heading': { return '' + this.inline.output(this.token.text) + '\n'; } case 'code': { if (this.options.highlight) { var code = this.options.highlight(this.token.text, this.token.lang); if (code != null && code !== this.token.text) { this.token.escaped = true; this.token.text = code; } } if (!this.token.escaped) { this.token.text = escape(this.token.text, true); } return '
'
                    + this.token.text
                    + '
\n'; } case 'table': { var body = '' , heading , i , row , cell , j; // header body += '\n\n'; for (i = 0; i < this.token.header.length; i++) { heading = this.inline.output(this.token.header[i]); body += this.token.align[i] ? '' + heading + '\n' : '' + heading + '\n'; } body += '\n\n'; // body body += '\n' for (i = 0; i < this.token.cells.length; i++) { row = this.token.cells[i]; body += '\n'; for (j = 0; j < row.length; j++) { cell = this.inline.output(row[j]); body += this.token.align[j] ? '' + cell + '\n' : '' + cell + '\n'; } body += '\n'; } body += '\n'; return '\n' + body + '
\n'; } case 'blockquote_start': { var body = ''; while (this.next().type !== 'blockquote_end') { body += this.tok(); } return '
\n' + body + '
\n'; } case 'list_start': { var type = this.token.ordered ? 'ol' : 'ul' , body = ''; while (this.next().type !== 'list_end') { body += this.tok(); } return '<' + type + '>\n' + body + '\n'; } case 'list_item_start': { var body = ''; while (this.next().type !== 'list_item_end') { body += this.token.type === 'text' ? this.parseText() : this.tok(); } return '
  • ' + body + '
  • \n'; } case 'loose_item_start': { var body = ''; while (this.next().type !== 'list_item_end') { body += this.tok(); } return '
  • ' + body + '
  • \n'; } case 'html': { return !this.token.pre && !this.options.pedantic ? this.inline.output(this.token.text) : this.token.text; } case 'paragraph': { return '

    ' + this.inline.output(this.token.text) + '

    \n'; } case 'text': { return '

    ' + this.parseText() + '

    \n'; } } }; /** * Helpers */ function escape(html, encode) { return html .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function shortenUrl(url) { if (url.length < 67) { return url; } else { return url.substring(0, 64) + "..."; } } function replace(regex, opt) { regex = regex.source; opt = opt || ''; return function self(name, val) { if (!name) return new RegExp(regex, opt); val = val.source || val; val = val.replace(/(^|[^\[])\^/g, '$1'); regex = regex.replace(name, val); return self; }; } function noop() {} noop.exec = noop; function merge(obj) { var i = 1 , target , key; for (; i < arguments.length; i++) { target = arguments[i]; for (key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { obj[key] = target[key]; } } } return obj; } /** * Marked */ function marked(src, opt) { try { if (opt) opt = merge({}, marked.defaults, opt); return Parser.parse(Lexer.lex(src, opt), opt); } catch (e) { e.message += '\nPlease report this to https://github.com/chjj/marked.'; if ((opt || marked.defaults).silent) { return '

    An error occured:

    '
                        + escape(e.message + '', true)
                        + '
    '; } throw e; } } /** * Options */ marked.options = marked.setOptions = function(opt) { merge(marked.defaults, opt); return marked; }; marked.defaults = { gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, // urls : [function(text, url){ return 'html'; }] smartLists: false, silent: false, highlight: null, langPrefix: 'lang-' }; /** * Expose */ marked.Parser = Parser; marked.parser = Parser.parse; marked.Lexer = Lexer; marked.lexer = Lexer.lex; marked.InlineLexer = InlineLexer; marked.inlineLexer = InlineLexer.output; marked.parse = marked; if (typeof exports === 'object') { module.exports = marked; } else if (typeof define === 'function' && define.amd) { define(function() { return marked; }); } else { this.marked = marked; } }).call(function() { return this || (typeof window !== 'undefined' ? window : global); }()); ================================================ FILE: mobile/www/app/shared/interceptor/auth.interceptor.js ================================================ (function() { 'use strict'; angular.module('tatami') .factory('authInterceptor', authInterceptor) .factory('authExpiredInterceptor', authExpiredInterceptor) .factory('endpointInterceptor', endpointInterceptor); authInterceptor.$inject = ['$rootScope', '$q', '$location', '$localStorage']; function authInterceptor($rootScope, $q, $location, $localStorage) { var interceptor = { request: request }; return interceptor; request.$inject = ['config']; function request(config) { config.headers = config.headers || {}; var token = $localStorage.get('token'); if (token && token.expires && token.expires > new Date().getTime()) { config.headers['x-auth-token'] = token.token; } return config; } } authExpiredInterceptor.$inject = ['$q', '$localStorage', '$injector']; function authExpiredInterceptor($q, $localStorage, $injector) { var interceptor = { responseError: responseError }; return interceptor; responseError.$inject = ['response']; function responseError(response) { if(response.status === 401 && (response.data.error == 'invalid_token' || response.data.error == 'Unauthorized')) { $localStorage.signOut(); var $state = $injector.get('$state'); $state.go('login'); } return $q.reject(response); } } /* endpointInterceptor built to replace PathService.buildPath(resource) * * Old implementation was causing issues - for some reason, REST path requests randomly started getting cached * so an attempt to access /rest/authentication wouldn't reach buildPath but would return the endpoint used initially * This interceptor never caches, so it's a better implementation. * * NOTE: Only make requests to server beginning with "/"; only access documents locally using the first folder and * NOT "/"; static url requests (i.e. to google.com) should begin with http://, NEVER "/". * * ONLY USE "/" AS FIRST CHARACTER OF REQUESTS IF YOU NEED THE ENDPOINT URL TO BE A PREFIX */ endpointInterceptor.$inject = ['$localStorage']; function endpointInterceptor($localStorage) { var interceptor = { request: request }; return interceptor; request.$inject = ['config']; function request(config) { if(config.url.indexOf("/") == 0) { config.url = $localStorage.get('endpoint').url + config.url; } return config; } } })(); ================================================ FILE: mobile/www/app/shared/providers/provider.js ================================================ (function() { 'use strict'; angular.module('tatami.providers', []); })(); ================================================ FILE: mobile/www/app/shared/providers/tatami.state.provider.js ================================================ (function() { 'use strict'; // This will dynamically create any tab substates inside the current tab. If in the timeline tab, we will // create a timeline.status state angular.module('tatami.providers') .provider('TatamiState', tatamiState); tatamiState.$inject = ['$stateProvider']; function tatamiState($stateProvider) { this.$get = tatamiStateHelper; function tatamiStateHelper() { var profileViewConfig = { templateUrl: 'app/shared/state/profile/profile.html', controller: 'ProfileCtrl', controllerAs: 'vm' }; var profileViews = []; profileViews['suggested@follow'] = { 'suggested@follow': profileViewConfig }; profileViews['following@follow'] = { 'following@follow': profileViewConfig }; profileViews['follower@follow'] = { 'follower@follow': profileViewConfig }; profileViews['timeline@home'] = { 'timeline@home': profileViewConfig }; profileViews['mentions@home'] = { 'mentions@home': profileViewConfig }; profileViews['favorites@home'] = { 'favorites@home': profileViewConfig }; profileViews['more@home'] = { 'more@home': profileViewConfig }; profileViews['company@home'] = { 'more@home': profileViewConfig }; profileViews['blockedusers@home'] = { 'more@home': profileViewConfig }; profileViews['allusers@home'] = { 'more@home': profileViewConfig }; var conversationViewConfig = { templateUrl: 'app/shared/state/conversation/conversation.html', controller: 'ConversationCtrl', controllerAs: 'vm' }; var conversationViews = []; conversationViews['suggested@follow'] = { 'suggested@follow': conversationViewConfig }; conversationViews['following@follow'] = { 'following@follow': conversationViewConfig }; conversationViews['follower@follow'] = { 'follower@follow': conversationViewConfig }; conversationViews['timeline@home'] = { 'timeline@home': conversationViewConfig }; conversationViews['mentions@home'] = { 'mentions@home': conversationViewConfig }; conversationViews['favorites@home'] = { 'favorites@home': conversationViewConfig }; conversationViews['company@home'] = { 'more@home': conversationViewConfig }; conversationViews['blockedusers@home'] = { 'more@home': profileViewConfig }; conversationViews['allusers@home'] = { 'more@home': profileViewConfig }; var tagViewConfig = { templateUrl: 'app/shared/state/tag/tag.html', controller: 'TagCtrl', controllerAs: 'vm' }; var tagViews = []; tagViews['suggested@follow'] = { 'suggested@follow': tagViewConfig }; tagViews['following@follow'] = { 'following@follow': tagViewConfig }; tagViews['follower@follow'] = { 'follower@follow': tagViewConfig }; tagViews['timeline@home'] = { 'timeline@home': tagViewConfig }; tagViews['mentions@home'] = { 'mentions@home': tagViewConfig }; tagViews['favorites@home'] = { 'favorites@home': tagViewConfig }; tagViews['company@home'] = { 'more@home': tagViewConfig }; tagViews['blockedusers@home'] = { 'more@home': profileViewConfig }; tagViews['allusers@home'] = { 'more@home': profileViewConfig }; var service = { addProfileState: addProfileState, addConversationState: addConversationState, addTagState: addTagState }; addProfileState.$inject = ['prefixName', 'parentName']; function addProfileState(prefixName, parentName) { $stateProvider.state(prefixName + '.profile', { url: '/profile/:username', views: profileViews[prefixName + '@' + parentName], resolve: { user: getUser, statuses: getStatuses, currentUser: getCurrentUser } }); getUser.$inject = ['UserService', '$stateParams']; function getUser(UserService, $stateParams) { return UserService.get({ username : $stateParams.username }).$promise; } getStatuses.$inject = ['user', 'StatusService']; function getStatuses(user, StatusService) { return StatusService.getUserTimeline({ username: user.username }).$promise; } getCurrentUser.$inject = ['currentUser']; function getCurrentUser(currentUser) { return currentUser; } } addConversationState.$inject = ['prefixName', 'parentName']; function addConversationState(prefixName, parentName) { $stateProvider.state(prefixName + '.conversation', { url: '/conversation/:statusId', views: conversationViews[prefixName + '@' + parentName], resolve: { originalStatus: getOriginalStatus, conversation: getConversation } }); getOriginalStatus.$inject = ['StatusService', '$stateParams']; function getOriginalStatus(StatusService, $stateParams) { return StatusService.get({ statusId : $stateParams.statusId }).$promise; } getConversation.$inject = ['StatusService', '$stateParams']; function getConversation(StatusService, $stateParams) { return StatusService.getDetails({ statusId : $stateParams.statusId }).$promise; } } addTagState.$inject = ['prefixName', 'parentName']; function addTagState(prefixName, parentName) { $stateProvider.state(prefixName + '.tag', { url: '/tag/:tag', views: tagViews[prefixName + '@' + parentName], resolve: { tag: getTag, statuses: getStatuses } }); getTag.$inject = ['$stateParams']; function getTag($stateParams) { return $stateParams.tag; } getStatuses.$inject = ['TagService', '$stateParams']; function getStatuses(TagService, $stateParams) { return TagService.getTagTimeline({ tag: $stateParams.tag }).$promise; } } return service; } } })(); ================================================ FILE: mobile/www/app/shared/services/HomeService.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('HomeService', homeService); homeService.$inject = ['$resource', 'PathService']; function homeService($resource, PathService) { return $resource(null, null, { 'getMentions': { method: 'GET', isArray: true, url: '/tatami/rest/mentions', transformResponse: responseTransform }, 'getFavorites': { method: 'GET', isArray: true, url: '/tatami/rest/favorites', transformResponse: responseTransform }, 'getCompanyTimeline': { method: 'GET', isArray: true, url: '/tatami/rest/company', transformResponse: responseTransform } }); responseTransform.$inject = ['statuses']; function responseTransform(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = PathService.getAvatar(statuses[i]); if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; } } })(); ================================================ FILE: mobile/www/app/shared/services/ProfileService.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('ProfileService', profileService); profileService.$inject = ['$resource', 'PathService']; function profileService($resource, PathService) { return $resource('/tatami/rest/account/profile', null, { 'get': { method: 'GET', transformResponse: function (profile) { profile = angular.fromJson(profile); profile['avatarURL'] = PathService.getAvatar(profile); return profile; } }, 'update': { method: 'PUT', transformRequest: function (profile) { delete profile['avatarURL']; return angular.toJson(profile); } } }); } })(); ================================================ FILE: mobile/www/app/shared/services/StatusService.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('StatusService', statusService); statusService.$inject = ['$resource', 'PathService']; function statusService($resource, PathService) { var responseTransform = function (statuses) { statuses = angular.fromJson(statuses); for (var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = PathService.getAvatar(statuses[i]); if (statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; }; return $resource('/tatami/rest/statuses/:statusId', null, { 'get': { method: 'GET', cache: false, transformResponse: function (status) { status = angular.fromJson(status); status.avatarURL = PathService.getAvatar(status); if (status.geoLocalization) { var latitude = status.geoLocalization.split(',')[0].trim(); var longitude = status.geoLocalization.split(',')[1].trim(); status['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } return status; } }, 'getHomeTimeline': { method: 'GET', isArray: true, url: '/tatami/rest/statuses/home_timeline', cache: false, transformResponse: responseTransform }, 'getUserTimeline': { method: 'GET', isArray: true, params: {username: '@username'}, url: '/tatami/rest/statuses/:username/timeline', cache: false, transformResponse: responseTransform }, 'getDetails': { method: 'GET', params: {statusId: '@statusId'}, url: '/tatami/rest/statuses/details/:statusId', cache: false, transformResponse: function (details) { details = angular.fromJson(details); for (var i = 0; i < details.discussionStatuses.length; i++) { details.discussionStatuses[i]['avatarURL'] = PathService.getAvatar(details.discussionStatuses[i]); if (details.discussionStatuses[i].geoLocalization) { var latitude = details.discussionStatuses[i].geoLocalization.split(',')[0].trim(); var longitude = details.discussionStatuses[i].geoLocalization.split(',')[1].trim(); details.discussionStatuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } for (var i = 0; i < details.sharedByLogins.length; i++) { details.sharedByLogins[i]['avatarURL'] = PathService.getAvatar(details.sharedByLogins[i]); } return details; } }, 'update': {method: 'PATCH', cache: false, params: {statusId: '@statusId'}}, 'announce': {method: 'PATCH', cache: false, params: {params: '@statusId'}}, 'hideStatus': { method: 'POST', params: {statusId: '@statusId'}, cache: false, url: '/tatami/rest/statuses/hide/:statusId' } }); } })(); ================================================ FILE: mobile/www/app/shared/services/UserService.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('UserService', userService); userService.$inject = ['$resource', 'PathService']; function userService($resource, PathService) { var responseTransform = function (users) { users = angular.fromJson(users); for (var i = 0; i < users.length; i++) { users[i]['avatarURL'] = PathService.getAvatar(users[i]); } return users; }; return $resource('/tatami/rest/users/:username', null, { 'get': { method: 'GET', params: {username: '@username'}, cache: false, transformResponse: function (user) { user = angular.fromJson(user); user['avatarURL'] = PathService.getAvatar(user); return user; } }, 'query': { method: 'GET', isArray: true, url: '/tatami/rest/users', transformResponse: responseTransform }, 'getFollowing': { method: 'GET', isArray: true, params: {username: '@username'}, url: '/tatami/rest/users/:username/friends', transformResponse: responseTransform }, 'getFollowers': { method: 'GET', isArray: true, params: {username: '@username'}, url: '/tatami/rest/users/:username/followers', transformResponse: responseTransform }, 'getSuggestions': { method: 'GET', isArray: true, url: '/tatami/rest/users/suggestions', transformResponse: function (suggestions) { suggestions = angular.fromJson(suggestions); for (var i = 0; i < suggestions.length; i++) { suggestions[i]['avatarURL'] = PathService.getAvatar(suggestions[i]); suggestions[i]['followingUser'] = false; } return suggestions; } }, 'follow': { method: 'PATCH', params: {username: '@username'} }, 'searchUsers': { method: 'GET', isArray: true, url: '/tatami/rest/users/:term', transformResponse: responseTransform }, 'deactivate': { method: 'PATCH', params: {username: '@username'} } }); } })(); ================================================ FILE: mobile/www/app/shared/services/account.service.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('AccountService', accountService); accountService.$inject = ['$resource', 'PathService']; function accountService($resource, PathService) { return $resource('/tatami/rest/account/admin', null, null); } })(); ================================================ FILE: mobile/www/app/shared/services/block.service.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('BlockService', blockService); blockService.$inject = ['$resource', 'PathService']; function blockService($resource, PathService) { var responseTransform = function (users) { users = angular.fromJson(users); for (var i = 0; i < users.length; i++) { users[i]['avatarURL'] = PathService.getAvatar(users[i]); } return users; }; return $resource(null, null, { 'getBlockedUsersForUser': { method: 'GET', isArray: true, params: {username: '@username'}, url: '/tatami/rest/block/blockedusers/:username', transformResponse: responseTransform }, 'updateBlockedUser': { method: 'PATCH', params: { username: '@username'}, url: '/tatami/rest/block/update/:username', transformResponse: function (blockedUser) { blockedUser = angular.fromJson(blockedUser); return blockedUser; } } }); } })(); ================================================ FILE: mobile/www/app/shared/services/localStorage.service.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('$localStorage', localStorage); localStorage.$inject = ['$window']; function localStorage($window) { var service = { get: getFromLocalStorage, set: setFromLocalStorage, signOut: clearToken }; return service; getFromLocalStorage.$inject = ['key']; function getFromLocalStorage(key) { if(isLocalStorageUndefined(key)) { return undefined; } return JSON.parse($window.localStorage[key] || '{}'); } setFromLocalStorage.$inject = ['key', 'value']; function setFromLocalStorage(key, value) { $window.localStorage[key] = JSON.stringify(value); } function clearToken() { $window.localStorage.removeItem('token'); } isLocalStorageUndefined.$inject = ['key']; function isLocalStorageUndefined(key) { return $window.localStorage.length === 0 || $window.localStorage[key] === 'undefined'; } } })(); ================================================ FILE: mobile/www/app/shared/services/path.service.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('PathService', avatarService); avatarService.$inject = ['TatamiEndpoint']; function avatarService(TatamiEndpoint) { var service = { getAvatar: getAvatar }; return service; getAvatar.$inject = ['user']; function getAvatar(user) { return TatamiEndpoint.getEndpoint().url + (user.avatar && user.avatar !== '' ? '/tatami/avatar/' + user.avatar + '/photo.jpg' : '/assets/img/default_image_profile.png'); } //buildPath() removed - REST endpoints being accessed were being cached, leading to issues when swapping endpoints //new implementation intercepts url requests beginning with "/" and tacks on the current endpoint url as a prefix } })(); ================================================ FILE: mobile/www/app/shared/services/report.service.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('ReportService', reportService); reportService.$inject = ['$resource', 'PathService']; function reportService($resource, PathService) { var responseTransform = function (statuses) { statuses = angular.fromJson(statuses); for (var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = PathService.getAvatar(statuses[i]); } return statuses; }; return $resource('/tatami/rest/statuses/report/:statusId', null, { 'reportStatus': { method: 'POST', params: {statusId: '@statusId'} }, 'getReportedStatuses': { method: 'GET', isArray: true, url: '/tatami/rest/statuses/report/reportedList', transformResponse: responseTransform }, 'approveStatus': { method : 'DELETE', params: {statusId: '@statusId'} }, 'deleteStatus': { method: 'PUT', params: {statusId: '@statusId'} } }); } })(); ================================================ FILE: mobile/www/app/shared/services/service.js ================================================ (function() { 'use strict'; angular.module('tatami.services', []); })(); ================================================ FILE: mobile/www/app/shared/services/tag.service.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('TagService', tagService); tagService.$inject = ['$resource', 'PathService']; function tagService($resource, PathService) { return $resource('/tatami/rest/tags', null, { 'get': { method:'GET', params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag' }, 'getTagTimeline': { method:'GET', isArray: true, params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag/tag_timeline', transformResponse: function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = PathService.getAvatar(statuses[i]); if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; } }, 'follow': { method:'PUT', params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag' }, 'getPopular': { method: 'GET', isArray: true, url: '/tatami/rest/tags/popular' } }); } })(); ================================================ FILE: mobile/www/app/shared/services/toast.service.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('ToastService', ToastService); ToastService.$inject = ['$window', '$translate', 'ionicToast']; function ToastService($window, $translate, ionicToast) { var service = { display: displayToast }; return service; function displayToast(toastMessage){ $translate(toastMessage).then(function(msg){ if (ionic.Platform.isIOS()){ ionicToast.show(msg, 'top', false, 2000); } else{ ionicToast.show(msg, 'bottom', false, 2000); } }); } } })(); ================================================ FILE: mobile/www/app/shared/state/conversation/conversation.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('ConversationCtrl', conversationCtrl); conversationCtrl.$inject = ['$ionicPopup', '$ionicHistory', '$state', 'originalStatus', 'conversation', 'currentUser']; function conversationCtrl($ionicPopup, $ionicHistory, $state, originalStatus, conversation, currentUser) { var vm = this; vm.conversation = buildStatusList(); vm.currentUser = currentUser; function buildStatusList() { try { return conversation.discussionStatuses.concat(originalStatus).sort(byDate); } catch (error) { var deletedPopup = $ionicPopup.alert({ title: 'Status Not Found!', template: 'The original status has been deleted.
    Returning to previous state.' }); deletedPopup.then(goBack); } } function goBack() { $ionicHistory.goBack(); } byDate.$inject = ['first', 'second']; function byDate(first, second) { return first.statusDate - second.statusDate; } } })(); ================================================ FILE: mobile/www/app/shared/state/conversation/conversation.html ================================================ ================================================ FILE: mobile/www/app/shared/state/profile/profile.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('ProfileCtrl', profileCtrl); profileCtrl.$inject = ['$ionicPopover', '$ionicPopup', '$scope', '$translate', 'user', 'statuses', 'currentUser', 'TatamiStatusRefresherService', 'UserService', 'BlockService']; function profileCtrl($ionicPopover, $ionicPopup, $scope, $translate, user, statuses, currentUser, TatamiStatusRefresherService, UserService, BlockService) { var vm = this; vm.user = user; vm.statuses = statuses; vm.currentUser = currentUser; vm.isCurrentUser = (vm.currentUser.username === vm.user.username); vm.customHeight = (vm.currentUser.isAdmin) ? {'height': '120px'} : {'height': '70px'}; //Adapts the height of the popover depending on the role because a different number of buttons is displayed vm.followUser = followUser; vm.getNewStatuses = getNewStatuses; vm.toggleActivateUser = toggleActivateUser; vm.blockUser = blockUser; function getNewStatuses() { return TatamiStatusRefresherService.refreshUserTimeline(user).then(setStatuses); } setStatuses.$inject = ['statuses']; function setStatuses(statuses) { vm.statuses = statuses; } function followUser() { UserService.follow({ username : vm.user.username }, { friend: !vm.user.friend, friendShip: true }, function() { vm.user.friend = !vm.user.friend; }); } function toggleActivateUser() { var confirmPopup; if (vm.user.activated) { confirmPopup = $ionicPopup.confirm({ title: $translate.instant('user.deactivate.title'), template: '' }); } else { confirmPopup = $ionicPopup.confirm({ title: $translate.instant('user.reactivate.title'), template: '' }); } confirmPopup.then(checkDelete); checkDelete.$inject = ['decision']; function checkDelete(decision) { if(decision) { UserService.deactivate({ username : vm.user.username }, {activate: true}, function() { vm.user.activated = !vm.user.activated; }); } } } function blockUser() { var confirmPopup = $ionicPopup.confirm({ title: $translate.instant('user.block.title'), template: '' }); confirmPopup.then(checkDelete); checkDelete.$inject = ['decision']; function checkDelete(decision) { if(decision) { BlockService.updateBlockedUser( {username: vm.user.username }, function () { ToastService.display('user.block.success'); } ); } } } $ionicPopover.fromTemplateUrl('app/shared/state/profile/userOptionsMenu.html', { scope: $scope }).then(function(popover) { $scope.popover = popover; }); } })(); ================================================ FILE: mobile/www/app/shared/state/profile/profile.html ================================================ ================================================ FILE: mobile/www/app/shared/state/profile/userOptionsMenu.html ================================================ ================================================ FILE: mobile/www/app/shared/state/tag/tag.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('TagCtrl', tagCtrl); tagCtrl.$inject = ['tag', 'statuses', 'currentUser', 'TatamiStatusRefresherService']; function tagCtrl(tag, statuses, currentUser, TatamiStatusRefresherService) { var vm = this; vm.tag = tag; vm.statuses = statuses; vm.currentUser = currentUser; vm.getNewStatuses = getNewStatuses; vm.getOldStatuses = getOldStatuses; function getNewStatuses() { return TatamiStatusRefresherService.refreshTagTimeline(vm.tag); } getOldStatuses.$inject = ['finalStatus']; function getOldStatuses(finalStatus) { return TatamiStatusRefresherService.getOldTags(finalStatus, vm.tag) } } })(); ================================================ FILE: mobile/www/app/shared/state/tag/tag.html ================================================ ================================================ FILE: mobile/www/app/shared/status/blockUserMenu.html ================================================ ================================================ FILE: mobile/www/app/shared/status/list/status-list.html ================================================
    ================================================ FILE: mobile/www/app/shared/status/list/status.list.directive.js ================================================ (function() { 'use strict'; angular.module('tatami') .directive('tatamiStatusList', tatamiStatusList); function tatamiStatusList() { var directive = { restrict: 'E', scope: { statuses: '=', currentUser: '=', tatamiRefresher: '&', tatamiInfiniteRefresher: '&' }, controller: controller, controllerAs: 'vm', templateUrl: 'app/shared/status/list/status-list.html' }; return directive; } controller.$inject = ['$scope', '$state']; function controller($scope, $state) { var vm = this; vm.statuses = $scope.statuses; vm.currentUser = $scope.currentUser; vm.$state = $state; vm.getNewStatuses = getNewStatuses; vm.getNewInfiniteScrollStatuses = getNewInfiniteScrollStatuses; vm.remove = remove; vm.finishedTimeline = vm.statuses && vm.statuses.length < 20; function getNewStatuses() { $scope.tatamiRefresher().then(setStatuses); } function getNewInfiniteScrollStatuses() { if(vm.statuses && vm.statuses.length > 0) { var lastStatus = vm.statuses[vm.statuses.length - 1].timelineId; $scope.tatamiInfiniteRefresher({ finalStatus: lastStatus }).then(addNewStatuses); } } remove.$inject = ['status']; function remove(status) { vm.statuses.splice(vm.statuses.indexOf(status), 1); } setStatuses.$inject = ['statuses']; function setStatuses(statuses) { vm.statuses = statuses; } addNewStatuses.$inject = ['oldStatuses']; function addNewStatuses(oldStatuses) { if(oldStatuses.length === 0) { vm.finishedTimeline = true; } vm.statuses.push.apply(vm.statuses, oldStatuses); } } })(); ================================================ FILE: mobile/www/app/shared/status/status.directive.js ================================================ (function() { 'use strict'; angular.module('tatami') .directive('tatamiStatus', tatamiStatus); function tatamiStatus() { var directive = { restrict: 'E', scope: { status: '=', currentUser: '=', onDelete: '&' }, controller: controller, controllerAs: 'vm', templateUrl: 'app/shared/status/status.html' }; return directive; } controller.$inject = ['$scope', '$state', '$ionicPopup', '$ionicPopover', '$filter', '$sce', 'StatusService', 'PathService', 'BlockService', '$translate', 'ReportService', 'ToastService', 'TatamiEndpoint']; function controller($scope, $state, $ionicPopup, $ionicPopover, $filter, $sce, StatusService, PathService, BlockService, $translate, ReportService, ToastService, TatamiEndpoint) { var vm = this; vm.state = $state.current.name; vm.status = $scope.status; vm.status.content = $filter('markdown')(vm.status.content); vm.currentUser = $scope.currentUser; vm.isAdmin = $scope.currentUser.isAdmin; vm.customHeight = (vm.isAdmin)? {'height': '270px'} : {'height': '170px'}; //Adapts the height of the popover depending on the role because a different number of buttons is displayed vm.remove = remove; vm.favorite = favorite; vm.isCurrentUser = !vm.currentUser || vm.currentUser.username === vm.status.username; vm.postReply = postReply; vm.goToConversation = goToConversation; vm.goToProfile = goToProfile; vm.goToTagTimeline = goToTagTimeline; vm.shareStatus = shareStatus; vm.buildAttachmentUrl = buildAttachmentUrl; vm.blockUser = blockUser; vm.reportStatus = reportStatus; vm.hideStatus = hideStatus; vm.announceStatus = announceStatus; vm.tatamEndpoint = TatamiEndpoint.getEndpoint().url; function remove() { var confirmPopup = $ionicPopup.confirm({ title: 'Delete', template: '' }); confirmPopup.then(checkDelete); checkDelete.$inject = ['decision']; function checkDelete(decision) { if(decision) { StatusService.delete({ statusId : vm.status.statusId }, function() { $scope.onDelete(vm.status); }); } } } function favorite() { StatusService.update({ statusId: vm.status.statusId }, { favorite: !vm.status.favorite }, function (status) { setStatus(status); if(vm.status.favorite){ ToastService.display('status.favorite.add'); } else { ToastService.display('status.favorite.remove'); } }); } function postReply() { $state.go('post', { statusId : vm.status.statusId }); } goToConversation.$inject = ['statusId']; function goToConversation(statusId) { var destinationState = $state.current.name.split('.')[0] + '.conversation'; $state.go(destinationState, { statusId : statusId }); } goToProfile.$inject = ['username']; function goToProfile(username) { var destinationState = $state.current.name.split('.')[0] + '.profile'; $state.go(destinationState, { username : username }); } goToTagTimeline.$inject = ['tag']; function goToTagTimeline(tag) { var destinationState = $state.current.name.split('.')[0] + '.tag'; $state.go(destinationState, { tag: tag }); } function shareStatus() { StatusService.update({statusId: vm.status.statusId}, {shared: !vm.status.shareByMe}, function(status){ setStatus(status); ToastService.display('status.share.toast'); }); } function reportStatus() { var confirmPopup = $ionicPopup.confirm({ title: 'Report Status', template: '' }); confirmPopup.then(reported); reported.$inject = ['decision']; function reported(decision) { if(decision) { ReportService.reportStatus({statusId: vm.status.statusId}, function () { ToastService.display('status.reportStatus.toast'); } ); } } } setStatus.$inject = ['status']; function setStatus(status) { vm.status = status; } buildAttachmentUrl.$inject = ['attachment']; function buildAttachmentUrl(attachment) { var location = TatamiEndpoint.getEndpoint().url; return location + '/tatami/file/' + attachment.attachmentId + '/' + attachment.filename; } $ionicPopover.fromTemplateUrl('app/shared/status/blockUserMenu.html', { scope: $scope }).then(function(popover) { $scope.popover = popover; }); function blockUser() { var confirmPopup = $ionicPopup.confirm({ title: 'Block User', template: '' }); confirmPopup.then(checkDelete); checkDelete.$inject = ['decision']; function checkDelete(decision) { if(decision) { BlockService.updateBlockedUser( {username: vm.status.username }, function () { ToastService.display('user.block.success'); } ); $state.go($state.current, {}, {reload: true}); // $scope.onDelete(vm.status); } } } function hideStatus() { StatusService.hideStatus({statusId: vm.status.statusId}, function () { $scope.onDelete(vm.status); ToastService.display('status.hide.toast'); }); } function announceStatus() { StatusService.update({statusId: vm.status.statusId}, {announced: true}, function(){ setStatus; ToastService.display('status.announcement.toast'); }); } } })(); ================================================ FILE: mobile/www/app/shared/status/status.html ================================================
        Your status has been shared New follower

    In reply to @{{ vm.status.replyToUsername }}

    {{ attachment.filename }}

    ================================================ FILE: mobile/www/app/shared/status/status.refresher.service.js ================================================ (function() { 'use strict'; angular.module('tatami') .factory('TatamiStatusRefresherService', tatamiStatusRefresherService); tatamiStatusRefresherService.$inject = ['$rootScope', 'StatusService', 'HomeService', 'TagService']; function tatamiStatusRefresherService($rootScope, StatusService, HomeService, TagService) { var service = { refreshHomeTimeline: refreshHomeTimeline, refreshCompanyTimeline: refreshCompanyTimeline, refreshMentions: refreshMentions, refreshFavorites: refreshFavorites, refreshUserTimeline: refreshUserTimeline, refreshTagTimeline: refreshTagTimeline, getOldFromHomeTimeline: getOldFromHomeTimeline, getOldFromCompanyTimeline: getOldFromCompanyTimeline, getOldMentions: getOldMentions, getOldTags: getOldTags }; return service; function refreshHomeTimeline() { return StatusService.getHomeTimeline().$promise.then(updateStatuses); } function refreshCompanyTimeline() { return HomeService.getCompanyTimeline().$promise.then(updateStatuses); } refreshUserTimeline.$inject = ['user']; function refreshUserTimeline(user) { return StatusService.getUserTimeline({ username : user.username }).$promise.then(updateStatuses); } function refreshMentions() { return HomeService.getMentions().$promise.then(updateStatuses); } function refreshFavorites() { return HomeService.getFavorites().$promise.then(updateStatuses); } refreshTagTimeline.$inject = ['tag']; function refreshTagTimeline(tag) { return TagService.getTagTimeline({ tag: tag }).$promise.then(updateStatuses); } getOldFromHomeTimeline.$inject = ['finalStatus']; function getOldFromHomeTimeline(finalStatus) { return StatusService.getHomeTimeline({ finish: finalStatus }).$promise.then(updateInfiniteStatuses); } getOldFromCompanyTimeline.$inject = ['finalStatus']; function getOldFromCompanyTimeline(finalStatus) { return HomeService.getCompanyTimeline({ finish: finalStatus }).$promise.then(updateInfiniteStatuses); } getOldMentions.$inject = ['finalStatus']; function getOldMentions(finalStatus) { return HomeService.getMentions({ finish: finalStatus }).$promise.then(updateInfiniteStatuses); } getOldTags.$inject = ['finalStatus', 'tag']; function getOldTags(finalStatus, tag) { return TagService.getTagTimeline({ tag: tag, finish: finalStatus }).$promise.then(updateInfiniteStatuses); } updateStatuses.$inject = ['statuses']; function updateStatuses(statuses) { $rootScope.$broadcast('scroll.refreshComplete'); return statuses; } updateInfiniteStatuses.$inject = ['statuses']; function updateInfiniteStatuses(statuses) { $rootScope.$broadcast('scroll.infiniteScrollComplete'); return statuses; } } })(); ================================================ FILE: mobile/www/app/shared/user/user-detail.html ================================================

    {{ vm.user.firstName + ' ' + vm.user.lastName }}

    @{{ vm.user.username }}

    ================================================ FILE: mobile/www/app/shared/user/user.detail.directive.js ================================================ (function() { 'use strict'; angular.module('tatami') .directive('tatamiUserDetail', tatamiUserDetail); function tatamiUserDetail() { var directive = { restrict: 'E', scope: { user: '=' }, controller: controller, controllerAs: 'vm', templateUrl: 'app/shared/user/user-detail.html' }; return directive; } controller.$inject = ['$scope']; function controller($scope) { var vm = this; vm.user = $scope.user; } })(); ================================================ FILE: mobile/www/app/shared/user/user.directive.js ================================================ (function() { 'use strict'; angular.module('tatami') .directive('tatamiUser', tatamiUser); tatamiUser.$inject = []; function tatamiUser() { var directive = { restrict: 'E', scope: { user: '=', currentUser: '=' }, controller: controller, controllerAs: 'vm', templateUrl: 'app/shared/user/user.html' }; return directive; } controller.$inject = ['$scope', '$state', 'UserService', 'BlockService', '$ionicPopup', '$ionicListDelegate']; function controller($scope, $state, UserService, BlockService, $ionicPopup, $ionicListDelegate) { var vm = this; vm.currentUser = $scope.currentUser; vm.user = $scope.user; vm.$state = $state; vm.followUser = followUser; vm.goToProfile = goToProfile; vm.updateBlockUser = updateBlockUser; vm.toggleActivateUser = toggleActivateUser; function followUser() { UserService.follow({ username : vm.user.username }, { friend: !vm.user.friend, friendShip: true }, function() { vm.user.friend = !vm.user.friend; }); } function goToProfile(username) { var destinationState = $state.current.name.split('.')[0] + '.profile'; $state.go(destinationState, { username : username }); $ionicListDelegate.closeOptionButtons(); } function updateBlockUser() { BlockService.updateBlockedUser( {username: vm.user.username }, function () { if(vm.user.blocked){ $ionicPopup.alert({ template: '' }); } else{ $ionicPopup.alert({ template: '' }); } vm.user.blocked = !vm.user.blocked; } ); $ionicListDelegate.closeOptionButtons(); } function toggleActivateUser() { var confirmPopup; if(vm.user.activated){ confirmPopup = $ionicPopup.confirm({ title: 'Deactivate user', template: '' }); } else { confirmPopup = $ionicPopup.confirm({ title: 'Activate user', template: '' }); } confirmPopup.then(toggle); toggle.$inject = ['decision']; function toggle(decision) { if(decision) { UserService.deactivate({ username : vm.user.username }, {activate: true}, function() { vm.user.activated = !vm.user.activated; } ); } } $ionicListDelegate.closeOptionButtons(); } } })(); ================================================ FILE: mobile/www/app/shared/user/user.html ================================================

    {{ vm.user.firstName }} {{ vm.user.lastName }}

    @{{ vm.user.username }}
    {{ (vm.user.friend ? 'user.unfollow' : 'user.follow') | translate }} {{ (vm.user.blocked ? 'user.unblock.title' : 'user.block.title') | translate }} {{ (vm.user.activated ? 'user.deactivate.title' : 'user.reactivate.title') | translate }}
    ================================================ FILE: mobile/www/app/shared/user/user.refresher.service.js ================================================ (function() { 'use strict'; angular.module('tatami.services') .factory('TatamiUserRefresherService', tatamiUserRefresherService); tatamiUserRefresherService.$inject = ['$rootScope', 'UserService']; function tatamiUserRefresherService($rootScope, UserService) { var service = { refreshSuggested: refreshSuggested, refreshFollowers: refreshFollowers, refreshFollowing: refreshFollowing, getNextUsers: getNextUsers, updateInfiniteUsers: updateInfiniteUsers }; return service; function refreshSuggested() { return UserService.getSuggestions().$promise.then(updateUsers); } refreshFollowers.$inject = ['currentUser']; function refreshFollowers(currentUser) { return UserService.getFollowers({ username: currentUser.username }).$promise.then(updateUsers); } refreshFollowing.$inject = ['currentUser']; function refreshFollowing(currentUser) { return UserService.getFollowing({ username: currentUser.username }).$promise.then(updateUsers); } updateUsers.$inject = ['users']; function updateUsers(users) { $rootScope.$broadcast('scroll.refreshComplete'); return users; } getNextUsers.$inject = ['usersCount']; function getNextUsers(usersCount) { return UserService.query({ pagination: usersCount }).$promise.then(updateInfiniteUsers); } updateInfiniteUsers.$inject = ['users']; function updateInfiniteUsers(users) { $rootScope.$broadcast('scroll.infiniteScrollComplete'); return users; } } })(); ================================================ FILE: mobile/www/app/shared/user/users.directive.js ================================================ (function() { 'use strict'; angular.module('tatami') .directive('tatamiUser', tatamiUser); tatamiUser.$inject = ['$state']; function tatamiUser($state) { var directive = { restrict: 'E', scope: { user: '=' }, controller: controller, controllerAs: 'vm', templateUrl: 'app/shared/user/users.html' }; return directive; } controller.$inject = ['$scope', '$state', 'UserService']; function controller($scope, $state, UserService) { var vm = this; vm.user = $scope.user; vm.followUser = followUser; function followUser() { UserService.follow({ username : vm.user.username }, { friend: !vm.user.friend, friendShip: true }, function() { $state.reload(); }); } } })(); ================================================ FILE: mobile/www/app/shared/user/users.html ================================================

    {{ vm.user.firstName }} {{ vm.user.lastName }}

    @{{ vm.user.username }}
    ================================================ FILE: mobile/www/app/tatami.controller.js ================================================ (function() { 'use strict'; angular.module('tatami') .controller('TatamiCtrl', tatamiCtrl); tatamiCtrl.$inject = ['$state']; function tatamiCtrl($state) { var vm = this; vm.$state = $state; } })(); ================================================ FILE: mobile/www/app/tatami.endpoint.js ================================================ (function() { 'use strict'; var defaultEndpoint = {url: 'http://tatami.ippon.fr'}; var endpoint; angular.module('tatami') .factory('TatamiEndpoint', tatamiEndpoint); tatamiEndpoint.$inject = ['$localStorage']; function tatamiEndpoint($localStorage) { var service = { getEndpoint: getEndpoint, setEndpoint: setEndpoint, getDefault: getDefaultEndpoint, reset: reset }; endpoint = $localStorage.get('endpoint'); if(!endpoint || !endpoint.url) { $localStorage.signOut(); endpoint = {url: defaultEndpoint.url}; $localStorage.set('endpoint', endpoint); } return service; function getEndpoint() { return endpoint; } setEndpoint.$inject = ['updated']; function setEndpoint(updated) { endpoint.url = updated; $localStorage.set('endpoint', endpoint); } function getDefaultEndpoint() { return defaultEndpoint; } function reset() { setEndpoint(defaultEndpoint.url); } } })(); ================================================ FILE: mobile/www/app/tatami.html ================================================
    ================================================ FILE: mobile/www/app/tatamiApp.js ================================================ (function() { 'use strict'; angular.module('tatami', [ 'ionic', 'tatami.services', 'tatami.providers', 'ngResource', 'ngCordova', 'hc.marked', 'pascalprecht.translate', 'ionic-toast' ]); angular.module('tatami') .run(tatamiRun) .config(tatamiConfig); tatamiRun.$inject = ['$ionicPlatform', '$state', '$localStorage', '$ionicHistory', '$translate', 'PathService', 'TatamiEndpoint', '$http']; function tatamiRun($ionicPlatform, $state, $localStorage, $ionicHistory, $translate, PathService, TatamiEndpoint, $http) { $ionicPlatform.ready(function () { // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard // for form inputs) if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) { cordova.plugins.Keyboard.disableScroll(true); } if (window.StatusBar) { // org.apache.cordova.statusbar required if ($ionicPlatform.is('android')) { StatusBar.backgroundColorByHexString('#444444'); } else { StatusBar.backgroundColorByName('white'); } } // first, the endpoint (when TatamiEndpoint is injected) gets set up or reset // based on whether or not it's been set before // then, we check is the (new) default endpoint live? $http({ url: '/tatami/rest/client/id', method: 'GET' }).then(function(result) { // if the endpoint is live... if (isValidToken()) { // ...and the token is valid, go to the timeline $state.go('timeline'); } else { // ...and the token is invalid, trash it and log the user out $localStorage.signOut(); logout(); } }, function(result) { // if the endpoint is invalid/down, the token is worthless now, so trash it, // log out, and reset endpoint to default $localStorage.signOut(); TatamiEndpoint.reset(); logout(); }); }); $ionicPlatform.on('resume', function resume() { if (!isValidToken()) { $localStorage.signOut(); logout(); } }); function logout() { $localStorage.signOut(); $ionicHistory.clearCache(); $state.go('login'); } function isValidToken() { var token = $localStorage.get('token'); return token && token.expires && token.expires > new Date().getTime() } var absolutePathChecker = new RegExp('^(?:[a-z]+:)?//', 'i'); document.onclick = function (e) { e = e || window.event; var element = e.target || e.srcElement; if (element.tagName == 'A' && absolutePathChecker.test(element.href)) { window.open(element.href, "_blank", "location=yes,presentationstyle=pagesheet,EnableViewPortScale=yes"); return false; } }; } tatamiConfig.$inject = [ '$resourceProvider', '$stateProvider', '$translateProvider', '$compileProvider', '$httpProvider' ]; function tatamiConfig($resourceProvider, $stateProvider, $translateProvider, $compileProvider, $httpProvider) { $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|tel):/); $resourceProvider.defaults.stripTrailingSlashes = false; $stateProvider // setup an abstract state for the tabs directive .state('tatami', { url: '', abstract: true, templateUrl: 'app/tatami.html', controller: 'TatamiCtrl', controllerAs: 'vm', resolve: { translatePartialLoader: getTranslationPartialLoader } }); getTranslationPartialLoader.$inject = ['$translate', '$translatePartialLoader']; function getTranslationPartialLoader($translate, $translatePartialLoader) { $translatePartialLoader.addPart('user'); $translatePartialLoader.addPart('status'); $translatePartialLoader.addPart('conversation'); return $translate.refresh(); } $httpProvider.interceptors.push('authInterceptor'); $httpProvider.interceptors.push('authExpiredInterceptor'); $httpProvider.interceptors.push('endpointInterceptor'); $translateProvider.useLoader('$translatePartialLoader', { urlTemplate: 'i18n/{lang}/{part}.json' }); var usedLanguage = window.localStorage.getItem('language') || navigator.language.split('-')[0] || 'en'; window.localStorage.setItem('language', usedLanguage); $translateProvider.preferredLanguage(usedLanguage); $translateProvider.use(usedLanguage); $translateProvider.useSanitizeValueStrategy('escaped'); $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); $compileProvider.directive('compile', compile); compile.$inject = ['$compile']; function compile($compile) { return directive; directive.$inject = ['scope', 'element', 'attrs']; function directive(scope, element, attrs) { var ensureCompileRunsOnce = scope.$watch( function(scope) { return scope.$eval(attrs.compile); }, function(value) { element.html(value); $compile(element.contents())(scope); ensureCompileRunsOnce(); } ); } } } })(); ================================================ FILE: mobile/www/css/ionic.app.css ================================================ @charset "UTF-8"; /* To customize the look and feel of Ionic, you can override the variables in ionic's _variables.scss file. For example, you might change some of the default colors: $light: #fff !default; $stable: #f8f8f8 !default; $positive: #387ef5 !default; $calm: #11c1f3 !default; $balanced: #33cd5f !default; $energized: #ffc900 !default; $assertive: #ef473a !default; $royal: #886aea !default; $dark: #444 !default; */ /*! * Copyright 2015 Drifty Co. * http://drifty.com/ * * Ionic, v1.2.4 * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * * By @maxlynch, @benjsperry, @adamdbradley <3 * * Licensed under the MIT license. Please see LICENSE for more information. * */ /*! Ionicons, v2.0.1 Created by Ben Sperry for the Ionic Framework, http://ionicons.com/ https://twitter.com/benjsperry https://twitter.com/ionicframework MIT License: https://github.com/driftyco/ionicons Android-style icons originally built by Google’s Material Design Icons: https://github.com/google/material-design-icons used under CC BY http://creativecommons.org/licenses/by/4.0/ Modified icons to fit ionicon’s grid from original. */ @font-face { font-family: "Ionicons"; src: url("../fonts/ionicons.eot?v=2.0.1"); src: url("../fonts/ionicons.eot?v=2.0.1#iefix") format("embedded-opentype"), url("../fonts/ionicons.ttf?v=2.0.1") format("truetype"), url("../fonts/ionicons.woff?v=2.0.1") format("woff"), url("../fonts/ionicons.woff") format("woff"), url("../fonts/ionicons.svg?v=2.0.1#Ionicons") format("svg"); font-weight: normal; font-style: normal; } .ion, .ionicons, .ion-alert:before, .ion-alert-circled:before, .ion-android-add:before, .ion-android-add-circle:before, .ion-android-alarm-clock:before, .ion-android-alert:before, .ion-android-apps:before, .ion-android-archive:before, .ion-android-arrow-back:before, .ion-android-arrow-down:before, .ion-android-arrow-dropdown:before, .ion-android-arrow-dropdown-circle:before, .ion-android-arrow-dropleft:before, .ion-android-arrow-dropleft-circle:before, .ion-android-arrow-dropright:before, .ion-android-arrow-dropright-circle:before, .ion-android-arrow-dropup:before, .ion-android-arrow-dropup-circle:before, .ion-android-arrow-forward:before, .ion-android-arrow-up:before, .ion-android-attach:before, .ion-android-bar:before, .ion-android-bicycle:before, .ion-android-boat:before, .ion-android-bookmark:before, .ion-android-bulb:before, .ion-android-bus:before, .ion-android-calendar:before, .ion-android-call:before, .ion-android-camera:before, .ion-android-cancel:before, .ion-android-car:before, .ion-android-cart:before, .ion-android-chat:before, .ion-android-checkbox:before, .ion-android-checkbox-blank:before, .ion-android-checkbox-outline:before, .ion-android-checkbox-outline-blank:before, .ion-android-checkmark-circle:before, .ion-android-clipboard:before, .ion-android-close:before, .ion-android-cloud:before, .ion-android-cloud-circle:before, .ion-android-cloud-done:before, .ion-android-cloud-outline:before, .ion-android-color-palette:before, .ion-android-compass:before, .ion-android-contact:before, .ion-android-contacts:before, .ion-android-contract:before, .ion-android-create:before, .ion-android-delete:before, .ion-android-desktop:before, .ion-android-document:before, .ion-android-done:before, .ion-android-done-all:before, .ion-android-download:before, .ion-android-drafts:before, .ion-android-exit:before, .ion-android-expand:before, .ion-android-favorite:before, .ion-android-favorite-outline:before, .ion-android-film:before, .ion-android-folder:before, .ion-android-folder-open:before, .ion-android-funnel:before, .ion-android-globe:before, .ion-android-hand:before, .ion-android-hangout:before, .ion-android-happy:before, .ion-android-home:before, .ion-android-image:before, .ion-android-laptop:before, .ion-android-list:before, .ion-android-locate:before, .platform-android .tatami-location:before, .ion-android-lock:before, .ion-android-mail:before, .ion-android-map:before, .ion-android-menu:before, .ion-android-microphone:before, .ion-android-microphone-off:before, .ion-android-more-horizontal:before, .ion-android-more-vertical:before, .ion-android-navigate:before, .ion-android-notifications:before, .ion-android-notifications-none:before, .ion-android-notifications-off:before, .ion-android-open:before, .ion-android-options:before, .ion-android-people:before, .ion-android-person:before, .ion-android-person-add:before, .ion-android-phone-landscape:before, .ion-android-phone-portrait:before, .ion-android-pin:before, .ion-android-plane:before, .ion-android-playstore:before, .ion-android-print:before, .ion-android-radio-button-off:before, .ion-android-radio-button-on:before, .ion-android-refresh:before, .ion-android-remove:before, .ion-android-remove-circle:before, .ion-android-restaurant:before, .ion-android-sad:before, .ion-android-search:before, .ion-android-send:before, .ion-android-settings:before, .ion-android-share:before, .ion-android-share-alt:before, .ion-android-star:before, .ion-android-star-half:before, .ion-android-star-outline:before, .ion-android-stopwatch:before, .ion-android-subway:before, .ion-android-sunny:before, .ion-android-sync:before, .ion-android-textsms:before, .ion-android-time:before, .ion-android-train:before, .ion-android-unlock:before, .ion-android-upload:before, .ion-android-volume-down:before, .ion-android-volume-mute:before, .ion-android-volume-off:before, .ion-android-volume-up:before, .ion-android-walk:before, .ion-android-warning:before, .ion-android-watch:before, .ion-android-wifi:before, .ion-aperture:before, .ion-archive:before, .ion-arrow-down-a:before, .ion-arrow-down-b:before, .ion-arrow-down-c:before, .ion-arrow-expand:before, .ion-arrow-graph-down-left:before, .ion-arrow-graph-down-right:before, .ion-arrow-graph-up-left:before, .ion-arrow-graph-up-right:before, .ion-arrow-left-a:before, .ion-arrow-left-b:before, .ion-arrow-left-c:before, .ion-arrow-move:before, .ion-arrow-resize:before, .ion-arrow-return-left:before, .ion-arrow-return-right:before, .ion-arrow-right-a:before, .ion-arrow-right-b:before, .ion-arrow-right-c:before, .ion-arrow-shrink:before, .ion-arrow-swap:before, .ion-arrow-up-a:before, .ion-arrow-up-b:before, .ion-arrow-up-c:before, .ion-asterisk:before, .ion-at:before, .ion-backspace:before, .ion-backspace-outline:before, .ion-bag:before, .ion-battery-charging:before, .ion-battery-empty:before, .ion-battery-full:before, .ion-battery-half:before, .ion-battery-low:before, .ion-beaker:before, .ion-beer:before, .ion-bluetooth:before, .ion-bonfire:before, .ion-bookmark:before, .ion-bowtie:before, .ion-briefcase:before, .ion-bug:before, .ion-calculator:before, .ion-calendar:before, .ion-camera:before, .ion-card:before, .ion-cash:before, .ion-chatbox:before, .ion-chatbox-working:before, .ion-chatboxes:before, .ion-chatbubble:before, .ion-chatbubble-working:before, .ion-chatbubbles:before, .ion-checkmark:before, .ion-checkmark-circled:before, .ion-checkmark-round:before, .ion-chevron-down:before, .ion-chevron-left:before, .ion-chevron-right:before, .ion-chevron-up:before, .ion-clipboard:before, .ion-clock:before, .ion-close:before, .ion-close-circled:before, .ion-close-round:before, .ion-closed-captioning:before, .ion-cloud:before, .ion-code:before, .ion-code-download:before, .ion-code-working:before, .ion-coffee:before, .ion-compass:before, .ion-compose:before, .ion-connection-bars:before, .ion-contrast:before, .ion-crop:before, .ion-cube:before, .ion-disc:before, .ion-document:before, .ion-document-text:before, .ion-drag:before, .ion-earth:before, .ion-easel:before, .ion-edit:before, .ion-egg:before, .ion-eject:before, .ion-email:before, .ion-email-unread:before, .ion-erlenmeyer-flask:before, .ion-erlenmeyer-flask-bubbles:before, .ion-eye:before, .ion-eye-disabled:before, .ion-female:before, .ion-filing:before, .ion-film-marker:before, .ion-fireball:before, .ion-flag:before, .ion-flame:before, .ion-flash:before, .ion-flash-off:before, .ion-folder:before, .ion-fork:before, .ion-fork-repo:before, .ion-forward:before, .ion-funnel:before, .ion-gear-a:before, .ion-gear-b:before, .ion-grid:before, .ion-hammer:before, .ion-happy:before, .ion-happy-outline:before, .ion-headphone:before, .ion-heart:before, .ion-heart-broken:before, .ion-help:before, .ion-help-buoy:before, .ion-help-circled:before, .ion-home:before, .ion-icecream:before, .ion-image:before, .ion-images:before, .ion-information:before, .ion-information-circled:before, .ion-ionic:before, .ion-ios-alarm:before, .ion-ios-alarm-outline:before, .ion-ios-albums:before, .ion-ios-albums-outline:before, .ion-ios-americanfootball:before, .ion-ios-americanfootball-outline:before, .ion-ios-analytics:before, .ion-ios-analytics-outline:before, .ion-ios-arrow-back:before, .ion-ios-arrow-down:before, .ion-ios-arrow-forward:before, .ion-ios-arrow-left:before, .ion-ios-arrow-right:before, .ion-ios-arrow-thin-down:before, .ion-ios-arrow-thin-left:before, .ion-ios-arrow-thin-right:before, .ion-ios-arrow-thin-up:before, .ion-ios-arrow-up:before, .ion-ios-at:before, .ion-ios-at-outline:before, .ion-ios-barcode:before, .ion-ios-barcode-outline:before, .ion-ios-baseball:before, .ion-ios-baseball-outline:before, .ion-ios-basketball:before, .ion-ios-basketball-outline:before, .ion-ios-bell:before, .ion-ios-bell-outline:before, .ion-ios-body:before, .ion-ios-body-outline:before, .ion-ios-bolt:before, .ion-ios-bolt-outline:before, .ion-ios-book:before, .ion-ios-book-outline:before, .ion-ios-bookmarks:before, .ion-ios-bookmarks-outline:before, .ion-ios-box:before, .ion-ios-box-outline:before, .ion-ios-briefcase:before, .ion-ios-briefcase-outline:before, .ion-ios-browsers:before, .ion-ios-browsers-outline:before, .ion-ios-calculator:before, .ion-ios-calculator-outline:before, .ion-ios-calendar:before, .ion-ios-calendar-outline:before, .ion-ios-camera:before, .ion-ios-camera-outline:before, .ion-ios-cart:before, .ion-ios-cart-outline:before, .ion-ios-chatboxes:before, .ion-ios-chatboxes-outline:before, .ion-ios-chatbubble:before, .ion-ios-chatbubble-outline:before, .ion-ios-checkmark:before, .ion-ios-checkmark-empty:before, .ion-ios-checkmark-outline:before, .ion-ios-circle-filled:before, .ion-ios-circle-outline:before, .ion-ios-clock:before, .ion-ios-clock-outline:before, .ion-ios-close:before, .ion-ios-close-empty:before, .ion-ios-close-outline:before, .ion-ios-cloud:before, .ion-ios-cloud-download:before, .ion-ios-cloud-download-outline:before, .ion-ios-cloud-outline:before, .ion-ios-cloud-upload:before, .ion-ios-cloud-upload-outline:before, .ion-ios-cloudy:before, .ion-ios-cloudy-night:before, .ion-ios-cloudy-night-outline:before, .ion-ios-cloudy-outline:before, .ion-ios-cog:before, .ion-ios-cog-outline:before, .ion-ios-color-filter:before, .ion-ios-color-filter-outline:before, .ion-ios-color-wand:before, .ion-ios-color-wand-outline:before, .ion-ios-compose:before, .ion-ios-compose-outline:before, .ion-ios-contact:before, .ion-ios-contact-outline:before, .ion-ios-copy:before, .ion-ios-copy-outline:before, .ion-ios-crop:before, .ion-ios-crop-strong:before, .ion-ios-download:before, .ion-ios-download-outline:before, .ion-ios-drag:before, .ion-ios-email:before, .ion-ios-email-outline:before, .ion-ios-eye:before, .ion-ios-eye-outline:before, .ion-ios-fastforward:before, .ion-ios-fastforward-outline:before, .ion-ios-filing:before, .ion-ios-filing-outline:before, .ion-ios-film:before, .ion-ios-film-outline:before, .ion-ios-flag:before, .ion-ios-flag-outline:before, .ion-ios-flame:before, .ion-ios-flame-outline:before, .ion-ios-flask:before, .ion-ios-flask-outline:before, .ion-ios-flower:before, .ion-ios-flower-outline:before, .ion-ios-folder:before, .ion-ios-folder-outline:before, .ion-ios-football:before, .ion-ios-football-outline:before, .ion-ios-game-controller-a:before, .ion-ios-game-controller-a-outline:before, .ion-ios-game-controller-b:before, .ion-ios-game-controller-b-outline:before, .ion-ios-gear:before, .ion-ios-gear-outline:before, .ion-ios-glasses:before, .ion-ios-glasses-outline:before, .ion-ios-grid-view:before, .ion-ios-grid-view-outline:before, .ion-ios-heart:before, .ion-ios-heart-outline:before, .ion-ios-help:before, .ion-ios-help-empty:before, .ion-ios-help-outline:before, .ion-ios-home:before, .ion-ios-home-outline:before, .ion-ios-infinite:before, .ion-ios-infinite-outline:before, .ion-ios-information:before, .ion-ios-information-empty:before, .ion-ios-information-outline:before, .ion-ios-ionic-outline:before, .ion-ios-keypad:before, .ion-ios-keypad-outline:before, .ion-ios-lightbulb:before, .ion-ios-lightbulb-outline:before, .ion-ios-list:before, .ion-ios-list-outline:before, .ion-ios-location:before, .platform-ios .tatami-location:before, .ion-ios-location-outline:before, .ion-ios-locked:before, .ion-ios-locked-outline:before, .ion-ios-loop:before, .ion-ios-loop-strong:before, .ion-ios-medical:before, .ion-ios-medical-outline:before, .ion-ios-medkit:before, .ion-ios-medkit-outline:before, .ion-ios-mic:before, .ion-ios-mic-off:before, .ion-ios-mic-outline:before, .ion-ios-minus:before, .ion-ios-minus-empty:before, .ion-ios-minus-outline:before, .ion-ios-monitor:before, .ion-ios-monitor-outline:before, .ion-ios-moon:before, .ion-ios-moon-outline:before, .ion-ios-more:before, .ion-ios-more-outline:before, .ion-ios-musical-note:before, .ion-ios-musical-notes:before, .ion-ios-navigate:before, .ion-ios-navigate-outline:before, .ion-ios-nutrition:before, .ion-ios-nutrition-outline:before, .ion-ios-paper:before, .ion-ios-paper-outline:before, .ion-ios-paperplane:before, .ion-ios-paperplane-outline:before, .ion-ios-partlysunny:before, .ion-ios-partlysunny-outline:before, .ion-ios-pause:before, .ion-ios-pause-outline:before, .ion-ios-paw:before, .ion-ios-paw-outline:before, .ion-ios-people:before, .ion-ios-people-outline:before, .ion-ios-person:before, .ion-ios-person-outline:before, .ion-ios-personadd:before, .ion-ios-personadd-outline:before, .ion-ios-photos:before, .ion-ios-photos-outline:before, .ion-ios-pie:before, .ion-ios-pie-outline:before, .ion-ios-pint:before, .ion-ios-pint-outline:before, .ion-ios-play:before, .ion-ios-play-outline:before, .ion-ios-plus:before, .ion-ios-plus-empty:before, .ion-ios-plus-outline:before, .ion-ios-pricetag:before, .ion-ios-pricetag-outline:before, .ion-ios-pricetags:before, .ion-ios-pricetags-outline:before, .ion-ios-printer:before, .ion-ios-printer-outline:before, .ion-ios-pulse:before, .ion-ios-pulse-strong:before, .ion-ios-rainy:before, .ion-ios-rainy-outline:before, .ion-ios-recording:before, .ion-ios-recording-outline:before, .ion-ios-redo:before, .ion-ios-redo-outline:before, .ion-ios-refresh:before, .ion-ios-refresh-empty:before, .ion-ios-refresh-outline:before, .ion-ios-reload:before, .ion-ios-reverse-camera:before, .ion-ios-reverse-camera-outline:before, .ion-ios-rewind:before, .ion-ios-rewind-outline:before, .ion-ios-rose:before, .ion-ios-rose-outline:before, .ion-ios-search:before, .ion-ios-search-strong:before, .ion-ios-settings:before, .ion-ios-settings-strong:before, .ion-ios-shuffle:before, .ion-ios-shuffle-strong:before, .ion-ios-skipbackward:before, .ion-ios-skipbackward-outline:before, .ion-ios-skipforward:before, .ion-ios-skipforward-outline:before, .ion-ios-snowy:before, .ion-ios-speedometer:before, .ion-ios-speedometer-outline:before, .ion-ios-star:before, .ion-ios-star-half:before, .ion-ios-star-outline:before, .ion-ios-stopwatch:before, .ion-ios-stopwatch-outline:before, .ion-ios-sunny:before, .ion-ios-sunny-outline:before, .ion-ios-telephone:before, .ion-ios-telephone-outline:before, .ion-ios-tennisball:before, .ion-ios-tennisball-outline:before, .ion-ios-thunderstorm:before, .ion-ios-thunderstorm-outline:before, .ion-ios-time:before, .ion-ios-time-outline:before, .ion-ios-timer:before, .ion-ios-timer-outline:before, .ion-ios-toggle:before, .ion-ios-toggle-outline:before, .ion-ios-trash:before, .ion-ios-trash-outline:before, .ion-ios-undo:before, .ion-ios-undo-outline:before, .ion-ios-unlocked:before, .ion-ios-unlocked-outline:before, .ion-ios-upload:before, .ion-ios-upload-outline:before, .ion-ios-videocam:before, .ion-ios-videocam-outline:before, .ion-ios-volume-high:before, .ion-ios-volume-low:before, .ion-ios-wineglass:before, .ion-ios-wineglass-outline:before, .ion-ios-world:before, .ion-ios-world-outline:before, .ion-ipad:before, .ion-iphone:before, .ion-ipod:before, .ion-jet:before, .ion-key:before, .ion-knife:before, .ion-laptop:before, .ion-leaf:before, .ion-levels:before, .ion-lightbulb:before, .ion-link:before, .ion-load-a:before, .ion-load-b:before, .ion-load-c:before, .ion-load-d:before, .ion-location:before, .ion-lock-combination:before, .ion-locked:before, .ion-log-in:before, .ion-log-out:before, .ion-loop:before, .ion-magnet:before, .ion-male:before, .ion-man:before, .ion-map:before, .ion-medkit:before, .ion-merge:before, .ion-mic-a:before, .ion-mic-b:before, .ion-mic-c:before, .ion-minus:before, .ion-minus-circled:before, .ion-minus-round:before, .ion-model-s:before, .ion-monitor:before, .ion-more:before, .ion-mouse:before, .ion-music-note:before, .ion-navicon:before, .ion-navicon-round:before, .ion-navigate:before, .ion-network:before, .ion-no-smoking:before, .ion-nuclear:before, .ion-outlet:before, .ion-paintbrush:before, .ion-paintbucket:before, .ion-paper-airplane:before, .ion-paperclip:before, .ion-pause:before, .ion-person:before, .ion-person-add:before, .ion-person-stalker:before, .ion-pie-graph:before, .ion-pin:before, .ion-pinpoint:before, .ion-pizza:before, .ion-plane:before, .ion-planet:before, .ion-play:before, .ion-playstation:before, .ion-plus:before, .ion-plus-circled:before, .ion-plus-round:before, .ion-podium:before, .ion-pound:before, .ion-power:before, .ion-pricetag:before, .ion-pricetags:before, .ion-printer:before, .ion-pull-request:before, .ion-qr-scanner:before, .ion-quote:before, .ion-radio-waves:before, .ion-record:before, .ion-refresh:before, .ion-reply:before, .ion-reply-all:before, .ion-ribbon-a:before, .ion-ribbon-b:before, .ion-sad:before, .ion-sad-outline:before, .ion-scissors:before, .ion-search:before, .ion-settings:before, .ion-share:before, .ion-shuffle:before, .ion-skip-backward:before, .ion-skip-forward:before, .ion-social-android:before, .ion-social-android-outline:before, .ion-social-angular:before, .ion-social-angular-outline:before, .ion-social-apple:before, .ion-social-apple-outline:before, .ion-social-bitcoin:before, .ion-social-bitcoin-outline:before, .ion-social-buffer:before, .ion-social-buffer-outline:before, .ion-social-chrome:before, .ion-social-chrome-outline:before, .ion-social-codepen:before, .ion-social-codepen-outline:before, .ion-social-css3:before, .ion-social-css3-outline:before, .ion-social-designernews:before, .ion-social-designernews-outline:before, .ion-social-dribbble:before, .ion-social-dribbble-outline:before, .ion-social-dropbox:before, .ion-social-dropbox-outline:before, .ion-social-euro:before, .ion-social-euro-outline:before, .ion-social-facebook:before, .ion-social-facebook-outline:before, .ion-social-foursquare:before, .ion-social-foursquare-outline:before, .ion-social-freebsd-devil:before, .ion-social-github:before, .ion-social-github-outline:before, .ion-social-google:before, .ion-social-google-outline:before, .ion-social-googleplus:before, .ion-social-googleplus-outline:before, .ion-social-hackernews:before, .ion-social-hackernews-outline:before, .ion-social-html5:before, .ion-social-html5-outline:before, .ion-social-instagram:before, .ion-social-instagram-outline:before, .ion-social-javascript:before, .ion-social-javascript-outline:before, .ion-social-linkedin:before, .ion-social-linkedin-outline:before, .ion-social-markdown:before, .ion-social-nodejs:before, .ion-social-octocat:before, .ion-social-pinterest:before, .ion-social-pinterest-outline:before, .ion-social-python:before, .ion-social-reddit:before, .ion-social-reddit-outline:before, .ion-social-rss:before, .ion-social-rss-outline:before, .ion-social-sass:before, .ion-social-skype:before, .ion-social-skype-outline:before, .ion-social-snapchat:before, .ion-social-snapchat-outline:before, .ion-social-tumblr:before, .ion-social-tumblr-outline:before, .ion-social-tux:before, .ion-social-twitch:before, .ion-social-twitch-outline:before, .ion-social-twitter:before, .ion-social-twitter-outline:before, .ion-social-usd:before, .ion-social-usd-outline:before, .ion-social-vimeo:before, .ion-social-vimeo-outline:before, .ion-social-whatsapp:before, .ion-social-whatsapp-outline:before, .ion-social-windows:before, .ion-social-windows-outline:before, .ion-social-wordpress:before, .ion-social-wordpress-outline:before, .ion-social-yahoo:before, .ion-social-yahoo-outline:before, .ion-social-yen:before, .ion-social-yen-outline:before, .ion-social-youtube:before, .ion-social-youtube-outline:before, .ion-soup-can:before, .ion-soup-can-outline:before, .ion-speakerphone:before, .ion-speedometer:before, .ion-spoon:before, .ion-star:before, .ion-stats-bars:before, .ion-steam:before, .ion-stop:before, .ion-thermometer:before, .ion-thumbsdown:before, .ion-thumbsup:before, .ion-toggle:before, .ion-toggle-filled:before, .ion-transgender:before, .ion-trash-a:before, .ion-trash-b:before, .ion-trophy:before, .ion-tshirt:before, .ion-tshirt-outline:before, .ion-umbrella:before, .ion-university:before, .ion-unlocked:before, .ion-upload:before, .ion-usb:before, .ion-videocamera:before, .ion-volume-high:before, .ion-volume-low:before, .ion-volume-medium:before, .ion-volume-mute:before, .ion-wand:before, .ion-waterdrop:before, .ion-wifi:before, .ion-wineglass:before, .ion-woman:before, .ion-wrench:before, .ion-xbox:before { display: inline-block; font-family: "Ionicons"; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; text-rendering: auto; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .ion-alert:before { content: ""; } .ion-alert-circled:before { content: ""; } .ion-android-add:before { content: ""; } .ion-android-add-circle:before { content: ""; } .ion-android-alarm-clock:before { content: ""; } .ion-android-alert:before { content: ""; } .ion-android-apps:before { content: ""; } .ion-android-archive:before { content: ""; } .ion-android-arrow-back:before { content: ""; } .ion-android-arrow-down:before { content: ""; } .ion-android-arrow-dropdown:before { content: ""; } .ion-android-arrow-dropdown-circle:before { content: ""; } .ion-android-arrow-dropleft:before { content: ""; } .ion-android-arrow-dropleft-circle:before { content: ""; } .ion-android-arrow-dropright:before { content: ""; } .ion-android-arrow-dropright-circle:before { content: ""; } .ion-android-arrow-dropup:before { content: ""; } .ion-android-arrow-dropup-circle:before { content: ""; } .ion-android-arrow-forward:before { content: ""; } .ion-android-arrow-up:before { content: ""; } .ion-android-attach:before { content: ""; } .ion-android-bar:before { content: ""; } .ion-android-bicycle:before { content: ""; } .ion-android-boat:before { content: ""; } .ion-android-bookmark:before { content: ""; } .ion-android-bulb:before { content: ""; } .ion-android-bus:before { content: ""; } .ion-android-calendar:before { content: ""; } .ion-android-call:before { content: ""; } .ion-android-camera:before { content: ""; } .ion-android-cancel:before { content: ""; } .ion-android-car:before { content: ""; } .ion-android-cart:before { content: ""; } .ion-android-chat:before { content: ""; } .ion-android-checkbox:before { content: ""; } .ion-android-checkbox-blank:before { content: ""; } .ion-android-checkbox-outline:before { content: ""; } .ion-android-checkbox-outline-blank:before { content: ""; } .ion-android-checkmark-circle:before { content: ""; } .ion-android-clipboard:before { content: ""; } .ion-android-close:before { content: ""; } .ion-android-cloud:before { content: ""; } .ion-android-cloud-circle:before { content: ""; } .ion-android-cloud-done:before { content: ""; } .ion-android-cloud-outline:before { content: ""; } .ion-android-color-palette:before { content: ""; } .ion-android-compass:before { content: ""; } .ion-android-contact:before { content: ""; } .ion-android-contacts:before { content: ""; } .ion-android-contract:before { content: ""; } .ion-android-create:before { content: ""; } .ion-android-delete:before { content: ""; } .ion-android-desktop:before { content: ""; } .ion-android-document:before { content: ""; } .ion-android-done:before { content: ""; } .ion-android-done-all:before { content: ""; } .ion-android-download:before { content: ""; } .ion-android-drafts:before { content: ""; } .ion-android-exit:before { content: ""; } .ion-android-expand:before { content: ""; } .ion-android-favorite:before { content: ""; } .ion-android-favorite-outline:before { content: ""; } .ion-android-film:before { content: ""; } .ion-android-folder:before { content: ""; } .ion-android-folder-open:before { content: ""; } .ion-android-funnel:before { content: ""; } .ion-android-globe:before { content: ""; } .ion-android-hand:before { content: ""; } .ion-android-hangout:before { content: ""; } .ion-android-happy:before { content: ""; } .ion-android-home:before { content: ""; } .ion-android-image:before { content: ""; } .ion-android-laptop:before { content: ""; } .ion-android-list:before { content: ""; } .ion-android-locate:before, .platform-android .tatami-location:before { content: ""; } .ion-android-lock:before { content: ""; } .ion-android-mail:before { content: ""; } .ion-android-map:before { content: ""; } .ion-android-menu:before { content: ""; } .ion-android-microphone:before { content: ""; } .ion-android-microphone-off:before { content: ""; } .ion-android-more-horizontal:before { content: ""; } .ion-android-more-vertical:before { content: ""; } .ion-android-navigate:before { content: ""; } .ion-android-notifications:before { content: ""; } .ion-android-notifications-none:before { content: ""; } .ion-android-notifications-off:before { content: ""; } .ion-android-open:before { content: ""; } .ion-android-options:before { content: ""; } .ion-android-people:before { content: ""; } .ion-android-person:before { content: ""; } .ion-android-person-add:before { content: ""; } .ion-android-phone-landscape:before { content: ""; } .ion-android-phone-portrait:before { content: ""; } .ion-android-pin:before { content: ""; } .ion-android-plane:before { content: ""; } .ion-android-playstore:before { content: ""; } .ion-android-print:before { content: ""; } .ion-android-radio-button-off:before { content: ""; } .ion-android-radio-button-on:before { content: ""; } .ion-android-refresh:before { content: ""; } .ion-android-remove:before { content: ""; } .ion-android-remove-circle:before { content: ""; } .ion-android-restaurant:before { content: ""; } .ion-android-sad:before { content: ""; } .ion-android-search:before { content: ""; } .ion-android-send:before { content: ""; } .ion-android-settings:before { content: ""; } .ion-android-share:before { content: ""; } .ion-android-share-alt:before { content: ""; } .ion-android-star:before { content: ""; } .ion-android-star-half:before { content: ""; } .ion-android-star-outline:before { content: ""; } .ion-android-stopwatch:before { content: ""; } .ion-android-subway:before { content: ""; } .ion-android-sunny:before { content: ""; } .ion-android-sync:before { content: ""; } .ion-android-textsms:before { content: ""; } .ion-android-time:before { content: ""; } .ion-android-train:before { content: ""; } .ion-android-unlock:before { content: ""; } .ion-android-upload:before { content: ""; } .ion-android-volume-down:before { content: ""; } .ion-android-volume-mute:before { content: ""; } .ion-android-volume-off:before { content: ""; } .ion-android-volume-up:before { content: ""; } .ion-android-walk:before { content: ""; } .ion-android-warning:before { content: ""; } .ion-android-watch:before { content: ""; } .ion-android-wifi:before { content: ""; } .ion-aperture:before { content: ""; } .ion-archive:before { content: ""; } .ion-arrow-down-a:before { content: ""; } .ion-arrow-down-b:before { content: ""; } .ion-arrow-down-c:before { content: ""; } .ion-arrow-expand:before { content: ""; } .ion-arrow-graph-down-left:before { content: ""; } .ion-arrow-graph-down-right:before { content: ""; } .ion-arrow-graph-up-left:before { content: ""; } .ion-arrow-graph-up-right:before { content: ""; } .ion-arrow-left-a:before { content: ""; } .ion-arrow-left-b:before { content: ""; } .ion-arrow-left-c:before { content: ""; } .ion-arrow-move:before { content: ""; } .ion-arrow-resize:before { content: ""; } .ion-arrow-return-left:before { content: ""; } .ion-arrow-return-right:before { content: ""; } .ion-arrow-right-a:before { content: ""; } .ion-arrow-right-b:before { content: ""; } .ion-arrow-right-c:before { content: ""; } .ion-arrow-shrink:before { content: ""; } .ion-arrow-swap:before { content: ""; } .ion-arrow-up-a:before { content: ""; } .ion-arrow-up-b:before { content: ""; } .ion-arrow-up-c:before { content: ""; } .ion-asterisk:before { content: ""; } .ion-at:before { content: ""; } .ion-backspace:before { content: ""; } .ion-backspace-outline:before { content: ""; } .ion-bag:before { content: ""; } .ion-battery-charging:before { content: ""; } .ion-battery-empty:before { content: ""; } .ion-battery-full:before { content: ""; } .ion-battery-half:before { content: ""; } .ion-battery-low:before { content: ""; } .ion-beaker:before { content: ""; } .ion-beer:before { content: ""; } .ion-bluetooth:before { content: ""; } .ion-bonfire:before { content: ""; } .ion-bookmark:before { content: ""; } .ion-bowtie:before { content: ""; } .ion-briefcase:before { content: ""; } .ion-bug:before { content: ""; } .ion-calculator:before { content: ""; } .ion-calendar:before { content: ""; } .ion-camera:before { content: ""; } .ion-card:before { content: ""; } .ion-cash:before { content: ""; } .ion-chatbox:before { content: ""; } .ion-chatbox-working:before { content: ""; } .ion-chatboxes:before { content: ""; } .ion-chatbubble:before { content: ""; } .ion-chatbubble-working:before { content: ""; } .ion-chatbubbles:before { content: ""; } .ion-checkmark:before { content: ""; } .ion-checkmark-circled:before { content: ""; } .ion-checkmark-round:before { content: ""; } .ion-chevron-down:before { content: ""; } .ion-chevron-left:before { content: ""; } .ion-chevron-right:before { content: ""; } .ion-chevron-up:before { content: ""; } .ion-clipboard:before { content: ""; } .ion-clock:before { content: ""; } .ion-close:before { content: ""; } .ion-close-circled:before { content: ""; } .ion-close-round:before { content: ""; } .ion-closed-captioning:before { content: ""; } .ion-cloud:before { content: ""; } .ion-code:before { content: ""; } .ion-code-download:before { content: ""; } .ion-code-working:before { content: ""; } .ion-coffee:before { content: ""; } .ion-compass:before { content: ""; } .ion-compose:before { content: ""; } .ion-connection-bars:before { content: ""; } .ion-contrast:before { content: ""; } .ion-crop:before { content: ""; } .ion-cube:before { content: ""; } .ion-disc:before { content: ""; } .ion-document:before { content: ""; } .ion-document-text:before { content: ""; } .ion-drag:before { content: ""; } .ion-earth:before { content: ""; } .ion-easel:before { content: ""; } .ion-edit:before { content: ""; } .ion-egg:before { content: ""; } .ion-eject:before { content: ""; } .ion-email:before { content: ""; } .ion-email-unread:before { content: ""; } .ion-erlenmeyer-flask:before { content: ""; } .ion-erlenmeyer-flask-bubbles:before { content: ""; } .ion-eye:before { content: ""; } .ion-eye-disabled:before { content: ""; } .ion-female:before { content: ""; } .ion-filing:before { content: ""; } .ion-film-marker:before { content: ""; } .ion-fireball:before { content: ""; } .ion-flag:before { content: ""; } .ion-flame:before { content: ""; } .ion-flash:before { content: ""; } .ion-flash-off:before { content: ""; } .ion-folder:before { content: ""; } .ion-fork:before { content: ""; } .ion-fork-repo:before { content: ""; } .ion-forward:before { content: ""; } .ion-funnel:before { content: ""; } .ion-gear-a:before { content: ""; } .ion-gear-b:before { content: ""; } .ion-grid:before { content: ""; } .ion-hammer:before { content: ""; } .ion-happy:before { content: ""; } .ion-happy-outline:before { content: ""; } .ion-headphone:before { content: ""; } .ion-heart:before { content: ""; } .ion-heart-broken:before { content: ""; } .ion-help:before { content: ""; } .ion-help-buoy:before { content: ""; } .ion-help-circled:before { content: ""; } .ion-home:before { content: ""; } .ion-icecream:before { content: ""; } .ion-image:before { content: ""; } .ion-images:before { content: ""; } .ion-information:before { content: ""; } .ion-information-circled:before { content: ""; } .ion-ionic:before { content: ""; } .ion-ios-alarm:before { content: ""; } .ion-ios-alarm-outline:before { content: ""; } .ion-ios-albums:before { content: ""; } .ion-ios-albums-outline:before { content: ""; } .ion-ios-americanfootball:before { content: ""; } .ion-ios-americanfootball-outline:before { content: ""; } .ion-ios-analytics:before { content: ""; } .ion-ios-analytics-outline:before { content: ""; } .ion-ios-arrow-back:before { content: ""; } .ion-ios-arrow-down:before { content: ""; } .ion-ios-arrow-forward:before { content: ""; } .ion-ios-arrow-left:before { content: ""; } .ion-ios-arrow-right:before { content: ""; } .ion-ios-arrow-thin-down:before { content: ""; } .ion-ios-arrow-thin-left:before { content: ""; } .ion-ios-arrow-thin-right:before { content: ""; } .ion-ios-arrow-thin-up:before { content: ""; } .ion-ios-arrow-up:before { content: ""; } .ion-ios-at:before { content: ""; } .ion-ios-at-outline:before { content: ""; } .ion-ios-barcode:before { content: ""; } .ion-ios-barcode-outline:before { content: ""; } .ion-ios-baseball:before { content: ""; } .ion-ios-baseball-outline:before { content: ""; } .ion-ios-basketball:before { content: ""; } .ion-ios-basketball-outline:before { content: ""; } .ion-ios-bell:before { content: ""; } .ion-ios-bell-outline:before { content: ""; } .ion-ios-body:before { content: ""; } .ion-ios-body-outline:before { content: ""; } .ion-ios-bolt:before { content: ""; } .ion-ios-bolt-outline:before { content: ""; } .ion-ios-book:before { content: ""; } .ion-ios-book-outline:before { content: ""; } .ion-ios-bookmarks:before { content: ""; } .ion-ios-bookmarks-outline:before { content: ""; } .ion-ios-box:before { content: ""; } .ion-ios-box-outline:before { content: ""; } .ion-ios-briefcase:before { content: ""; } .ion-ios-briefcase-outline:before { content: ""; } .ion-ios-browsers:before { content: ""; } .ion-ios-browsers-outline:before { content: ""; } .ion-ios-calculator:before { content: ""; } .ion-ios-calculator-outline:before { content: ""; } .ion-ios-calendar:before { content: ""; } .ion-ios-calendar-outline:before { content: ""; } .ion-ios-camera:before { content: ""; } .ion-ios-camera-outline:before { content: ""; } .ion-ios-cart:before { content: ""; } .ion-ios-cart-outline:before { content: ""; } .ion-ios-chatboxes:before { content: ""; } .ion-ios-chatboxes-outline:before { content: ""; } .ion-ios-chatbubble:before { content: ""; } .ion-ios-chatbubble-outline:before { content: ""; } .ion-ios-checkmark:before { content: ""; } .ion-ios-checkmark-empty:before { content: ""; } .ion-ios-checkmark-outline:before { content: ""; } .ion-ios-circle-filled:before { content: ""; } .ion-ios-circle-outline:before { content: ""; } .ion-ios-clock:before { content: ""; } .ion-ios-clock-outline:before { content: ""; } .ion-ios-close:before { content: ""; } .ion-ios-close-empty:before { content: ""; } .ion-ios-close-outline:before { content: ""; } .ion-ios-cloud:before { content: ""; } .ion-ios-cloud-download:before { content: ""; } .ion-ios-cloud-download-outline:before { content: ""; } .ion-ios-cloud-outline:before { content: ""; } .ion-ios-cloud-upload:before { content: ""; } .ion-ios-cloud-upload-outline:before { content: ""; } .ion-ios-cloudy:before { content: ""; } .ion-ios-cloudy-night:before { content: ""; } .ion-ios-cloudy-night-outline:before { content: ""; } .ion-ios-cloudy-outline:before { content: ""; } .ion-ios-cog:before { content: ""; } .ion-ios-cog-outline:before { content: ""; } .ion-ios-color-filter:before { content: ""; } .ion-ios-color-filter-outline:before { content: ""; } .ion-ios-color-wand:before { content: ""; } .ion-ios-color-wand-outline:before { content: ""; } .ion-ios-compose:before { content: ""; } .ion-ios-compose-outline:before { content: ""; } .ion-ios-contact:before { content: ""; } .ion-ios-contact-outline:before { content: ""; } .ion-ios-copy:before { content: ""; } .ion-ios-copy-outline:before { content: ""; } .ion-ios-crop:before { content: ""; } .ion-ios-crop-strong:before { content: ""; } .ion-ios-download:before { content: ""; } .ion-ios-download-outline:before { content: ""; } .ion-ios-drag:before { content: ""; } .ion-ios-email:before { content: ""; } .ion-ios-email-outline:before { content: ""; } .ion-ios-eye:before { content: ""; } .ion-ios-eye-outline:before { content: ""; } .ion-ios-fastforward:before { content: ""; } .ion-ios-fastforward-outline:before { content: ""; } .ion-ios-filing:before { content: ""; } .ion-ios-filing-outline:before { content: ""; } .ion-ios-film:before { content: ""; } .ion-ios-film-outline:before { content: ""; } .ion-ios-flag:before { content: ""; } .ion-ios-flag-outline:before { content: ""; } .ion-ios-flame:before { content: ""; } .ion-ios-flame-outline:before { content: ""; } .ion-ios-flask:before { content: ""; } .ion-ios-flask-outline:before { content: ""; } .ion-ios-flower:before { content: ""; } .ion-ios-flower-outline:before { content: ""; } .ion-ios-folder:before { content: ""; } .ion-ios-folder-outline:before { content: ""; } .ion-ios-football:before { content: ""; } .ion-ios-football-outline:before { content: ""; } .ion-ios-game-controller-a:before { content: ""; } .ion-ios-game-controller-a-outline:before { content: ""; } .ion-ios-game-controller-b:before { content: ""; } .ion-ios-game-controller-b-outline:before { content: ""; } .ion-ios-gear:before { content: ""; } .ion-ios-gear-outline:before { content: ""; } .ion-ios-glasses:before { content: ""; } .ion-ios-glasses-outline:before { content: ""; } .ion-ios-grid-view:before { content: ""; } .ion-ios-grid-view-outline:before { content: ""; } .ion-ios-heart:before { content: ""; } .ion-ios-heart-outline:before { content: ""; } .ion-ios-help:before { content: ""; } .ion-ios-help-empty:before { content: ""; } .ion-ios-help-outline:before { content: ""; } .ion-ios-home:before { content: ""; } .ion-ios-home-outline:before { content: ""; } .ion-ios-infinite:before { content: ""; } .ion-ios-infinite-outline:before { content: ""; } .ion-ios-information:before { content: ""; } .ion-ios-information-empty:before { content: ""; } .ion-ios-information-outline:before { content: ""; } .ion-ios-ionic-outline:before { content: ""; } .ion-ios-keypad:before { content: ""; } .ion-ios-keypad-outline:before { content: ""; } .ion-ios-lightbulb:before { content: ""; } .ion-ios-lightbulb-outline:before { content: ""; } .ion-ios-list:before { content: ""; } .ion-ios-list-outline:before { content: ""; } .ion-ios-location:before, .platform-ios .tatami-location:before { content: ""; } .ion-ios-location-outline:before { content: ""; } .ion-ios-locked:before { content: ""; } .ion-ios-locked-outline:before { content: ""; } .ion-ios-loop:before { content: ""; } .ion-ios-loop-strong:before { content: ""; } .ion-ios-medical:before { content: ""; } .ion-ios-medical-outline:before { content: ""; } .ion-ios-medkit:before { content: ""; } .ion-ios-medkit-outline:before { content: ""; } .ion-ios-mic:before { content: ""; } .ion-ios-mic-off:before { content: ""; } .ion-ios-mic-outline:before { content: ""; } .ion-ios-minus:before { content: ""; } .ion-ios-minus-empty:before { content: ""; } .ion-ios-minus-outline:before { content: ""; } .ion-ios-monitor:before { content: ""; } .ion-ios-monitor-outline:before { content: ""; } .ion-ios-moon:before { content: ""; } .ion-ios-moon-outline:before { content: ""; } .ion-ios-more:before { content: ""; } .ion-ios-more-outline:before { content: ""; } .ion-ios-musical-note:before { content: ""; } .ion-ios-musical-notes:before { content: ""; } .ion-ios-navigate:before { content: ""; } .ion-ios-navigate-outline:before { content: ""; } .ion-ios-nutrition:before { content: ""; } .ion-ios-nutrition-outline:before { content: ""; } .ion-ios-paper:before { content: ""; } .ion-ios-paper-outline:before { content: ""; } .ion-ios-paperplane:before { content: ""; } .ion-ios-paperplane-outline:before { content: ""; } .ion-ios-partlysunny:before { content: ""; } .ion-ios-partlysunny-outline:before { content: ""; } .ion-ios-pause:before { content: ""; } .ion-ios-pause-outline:before { content: ""; } .ion-ios-paw:before { content: ""; } .ion-ios-paw-outline:before { content: ""; } .ion-ios-people:before { content: ""; } .ion-ios-people-outline:before { content: ""; } .ion-ios-person:before { content: ""; } .ion-ios-person-outline:before { content: ""; } .ion-ios-personadd:before { content: ""; } .ion-ios-personadd-outline:before { content: ""; } .ion-ios-photos:before { content: ""; } .ion-ios-photos-outline:before { content: ""; } .ion-ios-pie:before { content: ""; } .ion-ios-pie-outline:before { content: ""; } .ion-ios-pint:before { content: ""; } .ion-ios-pint-outline:before { content: ""; } .ion-ios-play:before { content: ""; } .ion-ios-play-outline:before { content: ""; } .ion-ios-plus:before { content: ""; } .ion-ios-plus-empty:before { content: ""; } .ion-ios-plus-outline:before { content: ""; } .ion-ios-pricetag:before { content: ""; } .ion-ios-pricetag-outline:before { content: ""; } .ion-ios-pricetags:before { content: ""; } .ion-ios-pricetags-outline:before { content: ""; } .ion-ios-printer:before { content: ""; } .ion-ios-printer-outline:before { content: ""; } .ion-ios-pulse:before { content: ""; } .ion-ios-pulse-strong:before { content: ""; } .ion-ios-rainy:before { content: ""; } .ion-ios-rainy-outline:before { content: ""; } .ion-ios-recording:before { content: ""; } .ion-ios-recording-outline:before { content: ""; } .ion-ios-redo:before { content: ""; } .ion-ios-redo-outline:before { content: ""; } .ion-ios-refresh:before { content: ""; } .ion-ios-refresh-empty:before { content: ""; } .ion-ios-refresh-outline:before { content: ""; } .ion-ios-reload:before { content: ""; } .ion-ios-reverse-camera:before { content: ""; } .ion-ios-reverse-camera-outline:before { content: ""; } .ion-ios-rewind:before { content: ""; } .ion-ios-rewind-outline:before { content: ""; } .ion-ios-rose:before { content: ""; } .ion-ios-rose-outline:before { content: ""; } .ion-ios-search:before { content: ""; } .ion-ios-search-strong:before { content: ""; } .ion-ios-settings:before { content: ""; } .ion-ios-settings-strong:before { content: ""; } .ion-ios-shuffle:before { content: ""; } .ion-ios-shuffle-strong:before { content: ""; } .ion-ios-skipbackward:before { content: ""; } .ion-ios-skipbackward-outline:before { content: ""; } .ion-ios-skipforward:before { content: ""; } .ion-ios-skipforward-outline:before { content: ""; } .ion-ios-snowy:before { content: ""; } .ion-ios-speedometer:before { content: ""; } .ion-ios-speedometer-outline:before { content: ""; } .ion-ios-star:before { content: ""; } .ion-ios-star-half:before { content: ""; } .ion-ios-star-outline:before { content: ""; } .ion-ios-stopwatch:before { content: ""; } .ion-ios-stopwatch-outline:before { content: ""; } .ion-ios-sunny:before { content: ""; } .ion-ios-sunny-outline:before { content: ""; } .ion-ios-telephone:before { content: ""; } .ion-ios-telephone-outline:before { content: ""; } .ion-ios-tennisball:before { content: ""; } .ion-ios-tennisball-outline:before { content: ""; } .ion-ios-thunderstorm:before { content: ""; } .ion-ios-thunderstorm-outline:before { content: ""; } .ion-ios-time:before { content: ""; } .ion-ios-time-outline:before { content: ""; } .ion-ios-timer:before { content: ""; } .ion-ios-timer-outline:before { content: ""; } .ion-ios-toggle:before { content: ""; } .ion-ios-toggle-outline:before { content: ""; } .ion-ios-trash:before { content: ""; } .ion-ios-trash-outline:before { content: ""; } .ion-ios-undo:before { content: ""; } .ion-ios-undo-outline:before { content: ""; } .ion-ios-unlocked:before { content: ""; } .ion-ios-unlocked-outline:before { content: ""; } .ion-ios-upload:before { content: ""; } .ion-ios-upload-outline:before { content: ""; } .ion-ios-videocam:before { content: ""; } .ion-ios-videocam-outline:before { content: ""; } .ion-ios-volume-high:before { content: ""; } .ion-ios-volume-low:before { content: ""; } .ion-ios-wineglass:before { content: ""; } .ion-ios-wineglass-outline:before { content: ""; } .ion-ios-world:before { content: ""; } .ion-ios-world-outline:before { content: ""; } .ion-ipad:before { content: ""; } .ion-iphone:before { content: ""; } .ion-ipod:before { content: ""; } .ion-jet:before { content: ""; } .ion-key:before { content: ""; } .ion-knife:before { content: ""; } .ion-laptop:before { content: ""; } .ion-leaf:before { content: ""; } .ion-levels:before { content: ""; } .ion-lightbulb:before { content: ""; } .ion-link:before { content: ""; } .ion-load-a:before { content: ""; } .ion-load-b:before { content: ""; } .ion-load-c:before { content: ""; } .ion-load-d:before { content: ""; } .ion-location:before { content: ""; } .ion-lock-combination:before { content: ""; } .ion-locked:before { content: ""; } .ion-log-in:before { content: ""; } .ion-log-out:before { content: ""; } .ion-loop:before { content: ""; } .ion-magnet:before { content: ""; } .ion-male:before { content: ""; } .ion-man:before { content: ""; } .ion-map:before { content: ""; } .ion-medkit:before { content: ""; } .ion-merge:before { content: ""; } .ion-mic-a:before { content: ""; } .ion-mic-b:before { content: ""; } .ion-mic-c:before { content: ""; } .ion-minus:before { content: ""; } .ion-minus-circled:before { content: ""; } .ion-minus-round:before { content: ""; } .ion-model-s:before { content: ""; } .ion-monitor:before { content: ""; } .ion-more:before { content: ""; } .ion-mouse:before { content: ""; } .ion-music-note:before { content: ""; } .ion-navicon:before { content: ""; } .ion-navicon-round:before { content: ""; } .ion-navigate:before { content: ""; } .ion-network:before { content: ""; } .ion-no-smoking:before { content: ""; } .ion-nuclear:before { content: ""; } .ion-outlet:before { content: ""; } .ion-paintbrush:before { content: ""; } .ion-paintbucket:before { content: ""; } .ion-paper-airplane:before { content: ""; } .ion-paperclip:before { content: ""; } .ion-pause:before { content: ""; } .ion-person:before { content: ""; } .ion-person-add:before { content: ""; } .ion-person-stalker:before { content: ""; } .ion-pie-graph:before { content: ""; } .ion-pin:before { content: ""; } .ion-pinpoint:before { content: ""; } .ion-pizza:before { content: ""; } .ion-plane:before { content: ""; } .ion-planet:before { content: ""; } .ion-play:before { content: ""; } .ion-playstation:before { content: ""; } .ion-plus:before { content: ""; } .ion-plus-circled:before { content: ""; } .ion-plus-round:before { content: ""; } .ion-podium:before { content: ""; } .ion-pound:before { content: ""; } .ion-power:before { content: ""; } .ion-pricetag:before { content: ""; } .ion-pricetags:before { content: ""; } .ion-printer:before { content: ""; } .ion-pull-request:before { content: ""; } .ion-qr-scanner:before { content: ""; } .ion-quote:before { content: ""; } .ion-radio-waves:before { content: ""; } .ion-record:before { content: ""; } .ion-refresh:before { content: ""; } .ion-reply:before { content: ""; } .ion-reply-all:before { content: ""; } .ion-ribbon-a:before { content: ""; } .ion-ribbon-b:before { content: ""; } .ion-sad:before { content: ""; } .ion-sad-outline:before { content: ""; } .ion-scissors:before { content: ""; } .ion-search:before { content: ""; } .ion-settings:before { content: ""; } .ion-share:before { content: ""; } .ion-shuffle:before { content: ""; } .ion-skip-backward:before { content: ""; } .ion-skip-forward:before { content: ""; } .ion-social-android:before { content: ""; } .ion-social-android-outline:before { content: ""; } .ion-social-angular:before { content: ""; } .ion-social-angular-outline:before { content: ""; } .ion-social-apple:before { content: ""; } .ion-social-apple-outline:before { content: ""; } .ion-social-bitcoin:before { content: ""; } .ion-social-bitcoin-outline:before { content: ""; } .ion-social-buffer:before { content: ""; } .ion-social-buffer-outline:before { content: ""; } .ion-social-chrome:before { content: ""; } .ion-social-chrome-outline:before { content: ""; } .ion-social-codepen:before { content: ""; } .ion-social-codepen-outline:before { content: ""; } .ion-social-css3:before { content: ""; } .ion-social-css3-outline:before { content: ""; } .ion-social-designernews:before { content: ""; } .ion-social-designernews-outline:before { content: ""; } .ion-social-dribbble:before { content: ""; } .ion-social-dribbble-outline:before { content: ""; } .ion-social-dropbox:before { content: ""; } .ion-social-dropbox-outline:before { content: ""; } .ion-social-euro:before { content: ""; } .ion-social-euro-outline:before { content: ""; } .ion-social-facebook:before { content: ""; } .ion-social-facebook-outline:before { content: ""; } .ion-social-foursquare:before { content: ""; } .ion-social-foursquare-outline:before { content: ""; } .ion-social-freebsd-devil:before { content: ""; } .ion-social-github:before { content: ""; } .ion-social-github-outline:before { content: ""; } .ion-social-google:before { content: ""; } .ion-social-google-outline:before { content: ""; } .ion-social-googleplus:before { content: ""; } .ion-social-googleplus-outline:before { content: ""; } .ion-social-hackernews:before { content: ""; } .ion-social-hackernews-outline:before { content: ""; } .ion-social-html5:before { content: ""; } .ion-social-html5-outline:before { content: ""; } .ion-social-instagram:before { content: ""; } .ion-social-instagram-outline:before { content: ""; } .ion-social-javascript:before { content: ""; } .ion-social-javascript-outline:before { content: ""; } .ion-social-linkedin:before { content: ""; } .ion-social-linkedin-outline:before { content: ""; } .ion-social-markdown:before { content: ""; } .ion-social-nodejs:before { content: ""; } .ion-social-octocat:before { content: ""; } .ion-social-pinterest:before { content: ""; } .ion-social-pinterest-outline:before { content: ""; } .ion-social-python:before { content: ""; } .ion-social-reddit:before { content: ""; } .ion-social-reddit-outline:before { content: ""; } .ion-social-rss:before { content: ""; } .ion-social-rss-outline:before { content: ""; } .ion-social-sass:before { content: ""; } .ion-social-skype:before { content: ""; } .ion-social-skype-outline:before { content: ""; } .ion-social-snapchat:before { content: ""; } .ion-social-snapchat-outline:before { content: ""; } .ion-social-tumblr:before { content: ""; } .ion-social-tumblr-outline:before { content: ""; } .ion-social-tux:before { content: ""; } .ion-social-twitch:before { content: ""; } .ion-social-twitch-outline:before { content: ""; } .ion-social-twitter:before { content: ""; } .ion-social-twitter-outline:before { content: ""; } .ion-social-usd:before { content: ""; } .ion-social-usd-outline:before { content: ""; } .ion-social-vimeo:before { content: ""; } .ion-social-vimeo-outline:before { content: ""; } .ion-social-whatsapp:before { content: ""; } .ion-social-whatsapp-outline:before { content: ""; } .ion-social-windows:before { content: ""; } .ion-social-windows-outline:before { content: ""; } .ion-social-wordpress:before { content: ""; } .ion-social-wordpress-outline:before { content: ""; } .ion-social-yahoo:before { content: ""; } .ion-social-yahoo-outline:before { content: ""; } .ion-social-yen:before { content: ""; } .ion-social-yen-outline:before { content: ""; } .ion-social-youtube:before { content: ""; } .ion-social-youtube-outline:before { content: ""; } .ion-soup-can:before { content: ""; } .ion-soup-can-outline:before { content: ""; } .ion-speakerphone:before { content: ""; } .ion-speedometer:before { content: ""; } .ion-spoon:before { content: ""; } .ion-star:before { content: ""; } .ion-stats-bars:before { content: ""; } .ion-steam:before { content: ""; } .ion-stop:before { content: ""; } .ion-thermometer:before { content: ""; } .ion-thumbsdown:before { content: ""; } .ion-thumbsup:before { content: ""; } .ion-toggle:before { content: ""; } .ion-toggle-filled:before { content: ""; } .ion-transgender:before { content: ""; } .ion-trash-a:before { content: ""; } .ion-trash-b:before { content: ""; } .ion-trophy:before { content: ""; } .ion-tshirt:before { content: ""; } .ion-tshirt-outline:before { content: ""; } .ion-umbrella:before { content: ""; } .ion-university:before { content: ""; } .ion-unlocked:before { content: ""; } .ion-upload:before { content: ""; } .ion-usb:before { content: ""; } .ion-videocamera:before { content: ""; } .ion-volume-high:before { content: ""; } .ion-volume-low:before { content: ""; } .ion-volume-medium:before { content: ""; } .ion-volume-mute:before { content: ""; } .ion-wand:before { content: ""; } .ion-waterdrop:before { content: ""; } .ion-wifi:before { content: ""; } .ion-wineglass:before { content: ""; } .ion-woman:before { content: ""; } .ion-wrench:before { content: ""; } .ion-xbox:before { content: ""; } /** * Resets * -------------------------------------------------- * Adapted from normalize.css and some reset.css. We don't care even one * bit about old IE, so we don't need any hacks for that in here. * * There are probably other things we could remove here, as well. * * normalize.css v2.1.2 | MIT License | git.io/normalize * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) * http://cssreset.com */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, i, u, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, fieldset, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; vertical-align: baseline; font: inherit; font-size: 100%; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } /** * Prevent modern browsers from displaying `audio` without controls. * Remove excess height in iOS 5 devices. */ audio:not([controls]) { display: none; height: 0; } /** * Hide the `template` element in IE, Safari, and Firefox < 22. */ [hidden], template { display: none; } script { display: none !important; } /* ========================================================================== Base ========================================================================== */ /** * 1. Set default font family to sans-serif. * 2. Prevent iOS text size adjust after orientation change, without disabling * user zoom. */ html { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; font-family: sans-serif; /* 1 */ -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } /** * Remove default margin. */ body { margin: 0; line-height: 1; } /** * Remove default outlines. */ a, button, :focus, a:focus, button:focus, a:active, a:hover { outline: 0; } /* * * Remove tap highlight color */ a { -webkit-user-drag: none; -webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent; } a[href]:hover { cursor: pointer; } /* ========================================================================== Typography ========================================================================== */ /** * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ b, strong { font-weight: bold; } /** * Address styling not present in Safari 5 and Chrome. */ dfn { font-style: italic; } /** * Address differences between Firefox and other browsers. */ hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } /** * Correct font family set oddly in Safari 5 and Chrome. */ code, kbd, pre, samp { font-size: 1em; font-family: monospace, serif; } /** * Improve readability of pre-formatted text in all browsers. */ pre { white-space: pre-wrap; } /** * Set consistent quote types. */ q { quotes: "\201C" "\201D" "\2018" "\2019"; } /** * Address inconsistent and variable font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` affecting `line-height` in all browsers. */ sub, sup { position: relative; vertical-align: baseline; font-size: 75%; line-height: 0; } sup { top: -0.5em; } sub { bottom: -0.25em; } /** * Define consistent border, margin, and padding. */ fieldset { margin: 0 2px; padding: 0.35em 0.625em 0.75em; border: 1px solid #c0c0c0; } /** * 1. Correct `color` not being inherited in IE 8/9. * 2. Remove padding so people aren't caught out if they zero out fieldsets. */ legend { padding: 0; /* 2 */ border: 0; /* 1 */ } /** * 1. Correct font family not being inherited in all browsers. * 2. Correct font size not being inherited in all browsers. * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. * 4. Remove any default :focus styles * 5. Make sure webkit font smoothing is being inherited * 6. Remove default gradient in Android Firefox / FirefoxOS */ button, input, select, textarea { margin: 0; /* 3 */ font-size: 100%; /* 2 */ font-family: inherit; /* 1 */ outline-offset: 0; /* 4 */ outline-style: none; /* 4 */ outline-width: 0; /* 4 */ -webkit-font-smoothing: inherit; /* 5 */ background-image: none; /* 6 */ } /** * Address Firefox 4+ setting `line-height` on `input` using `importnt` in * the UA stylesheet. */ button, input { line-height: normal; } /** * Address inconsistent `text-transform` inheritance for `button` and `select`. * All other form control elements do not inherit `text-transform` values. * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. * Correct `select` style inheritance in Firefox 4+ and Opera. */ button, select { text-transform: none; } /** * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` * and `video` controls. * 2. Correct inability to style clickable `input` types in iOS. * 3. Improve usability and consistency of cursor style between image-type * `input` and others. */ button, html input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; /* 3 */ -webkit-appearance: button; /* 2 */ } /** * Re-set default cursor for disabled elements. */ button[disabled], html input[disabled] { cursor: default; } /** * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome * (include `-moz` to future-proof). */ input[type="search"] { -webkit-box-sizing: content-box; /* 2 */ -moz-box-sizing: content-box; box-sizing: content-box; -webkit-appearance: textfield; /* 1 */ } /** * Remove inner padding and search cancel button in Safari 5 and Chrome * on OS X. */ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /** * Remove inner padding and border in Firefox 4+. */ button::-moz-focus-inner, input::-moz-focus-inner { padding: 0; border: 0; } /** * 1. Remove default vertical scrollbar in IE 8/9. * 2. Improve readability and alignment in all browsers. */ textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } img { -webkit-user-drag: none; } /* ========================================================================== Tables ========================================================================== */ /** * Remove most spacing between table cells. */ table { border-spacing: 0; border-collapse: collapse; } /** * Scaffolding * -------------------------------------------------- */ *, *:before, *:after { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } html { overflow: hidden; -ms-touch-action: pan-y; touch-action: pan-y; } body, .ionic-body { -webkit-touch-callout: none; -webkit-font-smoothing: antialiased; font-smoothing: antialiased; -webkit-text-size-adjust: none; -moz-text-size-adjust: none; text-size-adjust: none; -webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; top: 0; right: 0; bottom: 0; left: 0; overflow: hidden; margin: 0; padding: 0; color: #000; word-wrap: break-word; font-size: 14px; font-family: -apple-system; font-family: "-apple-system", "Helvetica Neue", "Roboto", "Segoe UI", sans-serif; line-height: 20px; text-rendering: optimizeLegibility; -webkit-backface-visibility: hidden; -webkit-user-drag: none; -ms-content-zooming: none; } body.grade-b, body.grade-c { text-rendering: auto; } .content { position: relative; } .scroll-content { position: absolute; top: 0; right: 0; bottom: 0; left: 0; overflow: hidden; margin-top: -1px; padding-top: 1px; margin-bottom: -1px; width: auto; height: auto; } .menu .scroll-content.scroll-content-false { z-index: 11; } .scroll-view { position: relative; display: block; overflow: hidden; margin-top: -1px; } .scroll-view.overflow-scroll { position: relative; } .scroll-view.scroll-x { overflow-x: scroll; overflow-y: hidden; } .scroll-view.scroll-y { overflow-x: hidden; overflow-y: scroll; } .scroll-view.scroll-xy { overflow-x: scroll; overflow-y: scroll; } /** * Scroll is the scroll view component available for complex and custom * scroll view functionality. */ .scroll { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-touch-callout: none; -webkit-text-size-adjust: none; -moz-text-size-adjust: none; text-size-adjust: none; -webkit-transform-origin: left top; transform-origin: left top; } /** * Set ms-viewport to prevent MS "page squish" and allow fluid scrolling * https://msdn.microsoft.com/en-us/library/ie/hh869615(v=vs.85).aspx */ @-ms-viewport { width: device-width; } .scroll-bar { position: absolute; z-index: 9999; } .ng-animate .scroll-bar { visibility: hidden; } .scroll-bar-h { right: 2px; bottom: 3px; left: 2px; height: 3px; } .scroll-bar-h .scroll-bar-indicator { height: 100%; } .scroll-bar-v { top: 2px; right: 3px; bottom: 2px; width: 3px; } .scroll-bar-v .scroll-bar-indicator { width: 100%; } .scroll-bar-indicator { position: absolute; border-radius: 4px; background: rgba(0, 0, 0, 0.3); opacity: 1; -webkit-transition: opacity 0.3s linear; transition: opacity 0.3s linear; } .scroll-bar-indicator.scroll-bar-fade-out { opacity: 0; } .platform-android .scroll-bar-indicator { border-radius: 0; } .grade-b .scroll-bar-indicator, .grade-c .scroll-bar-indicator { background: #aaa; } .grade-b .scroll-bar-indicator.scroll-bar-fade-out, .grade-c .scroll-bar-indicator.scroll-bar-fade-out { -webkit-transition: none; transition: none; } ion-infinite-scroll { height: 60px; width: 100%; display: block; display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-direction: normal; -webkit-box-orient: horizontal; -webkit-flex-direction: row; -moz-flex-direction: row; -ms-flex-direction: row; flex-direction: row; -webkit-box-pack: center; -ms-flex-pack: center; -webkit-justify-content: center; -moz-justify-content: center; justify-content: center; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; } ion-infinite-scroll .icon { color: #666666; font-size: 30px; color: #666666; } ion-infinite-scroll:not(.active) .spinner, ion-infinite-scroll:not(.active) .icon:before { display: none; } .overflow-scroll { overflow-x: hidden; overflow-y: scroll; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; top: 0; right: 0; bottom: 0; left: 0; position: absolute; } .overflow-scroll.pane { overflow-x: hidden; overflow-y: scroll; } .overflow-scroll .scroll { position: static; height: 100%; -webkit-transform: translate3d(0, 0, 0); } .overflow-scroll.keyboard-up:not(.keyboard-up-confirm) { overflow: hidden; } /* If you change these, change platform.scss as well */ .has-header { top: 44px; } .no-header { top: 0; } .has-subheader { top: 88px; } .has-tabs-top { top: 93px; } .has-header.has-subheader.has-tabs-top { top: 137px; } .has-footer { bottom: 44px; } .has-subfooter { bottom: 88px; } .has-tabs, .bar-footer.has-tabs { bottom: 49px; } .has-tabs.pane, .bar-footer.has-tabs.pane { bottom: 49px; height: auto; } .bar-subfooter.has-tabs { bottom: 93px; } .has-footer.has-tabs { bottom: 93px; } .pane { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); -webkit-transition-duration: 0; transition-duration: 0; z-index: 1; } .view { z-index: 1; } .pane, .view { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; background-color: #fff; overflow: hidden; } .view-container { position: absolute; display: block; width: 100%; height: 100%; } /** * Typography * -------------------------------------------------- */ p { margin: 0 0 10px; } small { font-size: 85%; } cite { font-style: normal; } .text-left { text-align: left; } .text-right { text-align: right; } .text-center { text-align: center; } h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { color: #000; font-weight: 500; font-family: "-apple-system", "Helvetica Neue", "Roboto", "Segoe UI", sans-serif; line-height: 1.2; } h1 small, h2 small, h3 small, h4 small, h5 small, h6 small, .h1 small, .h2 small, .h3 small, .h4 small, .h5 small, .h6 small { font-weight: normal; line-height: 1; } h1, .h1, h2, .h2, h3, .h3 { margin-top: 20px; margin-bottom: 10px; } h1:first-child, .h1:first-child, h2:first-child, .h2:first-child, h3:first-child, .h3:first-child { margin-top: 0; } h1 + h1, h1 + .h1, h1 + h2, h1 + .h2, h1 + h3, h1 + .h3, .h1 + h1, .h1 + .h1, .h1 + h2, .h1 + .h2, .h1 + h3, .h1 + .h3, h2 + h1, h2 + .h1, h2 + h2, h2 + .h2, h2 + h3, h2 + .h3, .h2 + h1, .h2 + .h1, .h2 + h2, .h2 + .h2, .h2 + h3, .h2 + .h3, h3 + h1, h3 + .h1, h3 + h2, h3 + .h2, h3 + h3, h3 + .h3, .h3 + h1, .h3 + .h1, .h3 + h2, .h3 + .h2, .h3 + h3, .h3 + .h3 { margin-top: 10px; } h4, .h4, h5, .h5, h6, .h6 { margin-top: 10px; margin-bottom: 10px; } h1, .h1 { font-size: 36px; } h2, .h2 { font-size: 30px; } h3, .h3 { font-size: 24px; } h4, .h4 { font-size: 18px; } h5, .h5 { font-size: 14px; } h6, .h6 { font-size: 12px; } h1 small, .h1 small { font-size: 24px; } h2 small, .h2 small { font-size: 18px; } h3 small, .h3 small, h4 small, .h4 small { font-size: 14px; } dl { margin-bottom: 20px; } dt, dd { line-height: 1.42857; } dt { font-weight: bold; } blockquote { margin: 0 0 20px; padding: 10px 20px; border-left: 5px solid gray; } blockquote p { font-weight: 300; font-size: 17.5px; line-height: 1.25; } blockquote p:last-child { margin-bottom: 0; } blockquote small { display: block; line-height: 1.42857; } blockquote small:before { content: '\2014 \00A0'; } q:before, q:after, blockquote:before, blockquote:after { content: ""; } address { display: block; margin-bottom: 20px; font-style: normal; line-height: 1.42857; } a { color: #387ef5; } a.subdued { padding-right: 10px; color: #888; text-decoration: none; } a.subdued:hover { text-decoration: none; } a.subdued:last-child { padding-right: 0; } /** * Action Sheets * -------------------------------------------------- */ .action-sheet-backdrop { -webkit-transition: background-color 150ms ease-in-out; transition: background-color 150ms ease-in-out; position: fixed; top: 0; left: 0; z-index: 11; width: 100%; height: 100%; background-color: transparent; } .action-sheet-backdrop.active { background-color: rgba(0, 0, 0, 0.4); } .action-sheet-wrapper { -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); -webkit-transition: all cubic-bezier(0.36, 0.66, 0.04, 1) 500ms; transition: all cubic-bezier(0.36, 0.66, 0.04, 1) 500ms; position: absolute; bottom: 0; left: 0; right: 0; width: 100%; max-width: 500px; margin: auto; } .action-sheet-up { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } .action-sheet { margin-left: 8px; margin-right: 8px; width: auto; z-index: 11; overflow: hidden; } .action-sheet .button { display: block; padding: 1px; width: 100%; border-radius: 0; border-color: #d1d3d6; background-color: transparent; color: #007aff; font-size: 21px; } .action-sheet .button:hover { color: #007aff; } .action-sheet .button.destructive { color: #ff3b30; } .action-sheet .button.destructive:hover { color: #ff3b30; } .action-sheet .button.active, .action-sheet .button.activated { box-shadow: none; border-color: #d1d3d6; color: #007aff; background: #e4e5e7; } .action-sheet-has-icons .icon { position: absolute; left: 16px; } .action-sheet-title { padding: 16px; color: #8f8f8f; text-align: center; font-size: 13px; } .action-sheet-group { margin-bottom: 8px; border-radius: 4px; background-color: #fff; overflow: hidden; } .action-sheet-group .button { border-width: 1px 0px 0px 0px; } .action-sheet-group .button:first-child:last-child { border-width: 0; } .action-sheet-options { background: #f1f2f3; } .action-sheet-cancel .button { font-weight: 500; } .action-sheet-open { pointer-events: none; } .action-sheet-open.modal-open .modal { pointer-events: none; } .action-sheet-open .action-sheet-backdrop { pointer-events: auto; } .platform-android .action-sheet-backdrop.active { background-color: rgba(0, 0, 0, 0.2); } .platform-android .action-sheet { margin: 0; } .platform-android .action-sheet .action-sheet-title, .platform-android .action-sheet .button { text-align: left; border-color: transparent; font-size: 16px; color: inherit; } .platform-android .action-sheet .action-sheet-title { font-size: 14px; padding: 16px; color: #666; } .platform-android .action-sheet .button.active, .platform-android .action-sheet .button.activated { background: #e8e8e8; } .platform-android .action-sheet-group { margin: 0; border-radius: 0; background-color: #fafafa; } .platform-android .action-sheet-cancel { display: none; } .platform-android .action-sheet-has-icons .button { padding-left: 56px; } .backdrop { position: fixed; top: 0; left: 0; z-index: 11; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); visibility: hidden; opacity: 0; -webkit-transition: 0.1s opacity linear; transition: 0.1s opacity linear; } .backdrop.visible { visibility: visible; } .backdrop.active { opacity: 1; } /** * Bar (Headers and Footers) * -------------------------------------------------- */ .bar { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; right: 0; left: 0; z-index: 9; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; padding: 5px; width: 100%; height: 44px; border-width: 0; border-style: solid; border-top: 1px solid transparent; border-bottom: 1px solid #ddd; background-color: white; /* border-width: 1px will actually create 2 device pixels on retina */ /* this nifty trick sets an actual 1px border on hi-res displays */ background-size: 0; } @media (min--moz-device-pixel-ratio: 1.5), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 144dpi), (min-resolution: 1.5dppx) { .bar { border: none; background-image: linear-gradient(0deg, #ddd, #ddd 50%, transparent 50%); background-position: bottom; background-size: 100% 1px; background-repeat: no-repeat; } } .bar.bar-clear { border: none; background: none; color: #fff; } .bar.bar-clear .button { color: #fff; } .bar.bar-clear .title { color: #fff; } .bar.item-input-inset .item-input-wrapper { margin-top: -1px; } .bar.item-input-inset .item-input-wrapper input { padding-left: 8px; width: 94%; height: 28px; background: transparent; } .bar.bar-light { border-color: #ddd; background-color: white; background-image: linear-gradient(0deg, #ddd, #ddd 50%, transparent 50%); color: #444; } .bar.bar-light .title { color: #444; } .bar.bar-light.bar-footer { background-image: linear-gradient(180deg, #ddd, #ddd 50%, transparent 50%); } .bar.bar-stable { border-color: #b2b2b2; background-color: #f8f8f8; background-image: linear-gradient(0deg, #b2b2b2, #b2b2b2 50%, transparent 50%); color: #444; } .bar.bar-stable .title { color: #444; } .bar.bar-stable.bar-footer { background-image: linear-gradient(180deg, #b2b2b2, #b2b2b2 50%, transparent 50%); } .bar.bar-positive { border-color: #0c60ee; background-color: #387ef5; background-image: linear-gradient(0deg, #0c60ee, #0c60ee 50%, transparent 50%); color: #fff; } .bar.bar-positive .title { color: #fff; } .bar.bar-positive.bar-footer { background-image: linear-gradient(180deg, #0c60ee, #0c60ee 50%, transparent 50%); } .bar.bar-calm { border-color: #0a9dc7; background-color: #11c1f3; background-image: linear-gradient(0deg, #0a9dc7, #0a9dc7 50%, transparent 50%); color: #fff; } .bar.bar-calm .title { color: #fff; } .bar.bar-calm.bar-footer { background-image: linear-gradient(180deg, #0a9dc7, #0a9dc7 50%, transparent 50%); } .bar.bar-assertive { border-color: #e42112; background-color: #ef473a; background-image: linear-gradient(0deg, #e42112, #e42112 50%, transparent 50%); color: #fff; } .bar.bar-assertive .title { color: #fff; } .bar.bar-assertive.bar-footer { background-image: linear-gradient(180deg, #e42112, #e42112 50%, transparent 50%); } .bar.bar-balanced { border-color: #28a54c; background-color: #33cd5f; background-image: linear-gradient(0deg, #28a54c, #28a54c 50%, transparent 50%); color: #fff; } .bar.bar-balanced .title { color: #fff; } .bar.bar-balanced.bar-footer { background-image: linear-gradient(180deg, #28a54c, #0c60ee 50%, transparent 50%); } .bar.bar-energized { border-color: #e6b500; background-color: #ffc900; background-image: linear-gradient(0deg, #e6b500, #e6b500 50%, transparent 50%); color: #fff; } .bar.bar-energized .title { color: #fff; } .bar.bar-energized.bar-footer { background-image: linear-gradient(180deg, #e6b500, #e6b500 50%, transparent 50%); } .bar.bar-royal { border-color: #6b46e5; background-color: #886aea; background-image: linear-gradient(0deg, #6b46e5, #6b46e5 50%, transparent 50%); color: #fff; } .bar.bar-royal .title { color: #fff; } .bar.bar-royal.bar-footer { background-image: linear-gradient(180deg, #6b46e5, #6b46e5 50%, transparent 50%); } .bar.bar-dark { border-color: #111; background-color: #444444; background-image: linear-gradient(0deg, #111, #111 50%, transparent 50%); color: #fff; } .bar.bar-dark .title { color: #fff; } .bar.bar-dark.bar-footer { background-image: linear-gradient(180deg, #111, #111 50%, transparent 50%); } .bar .title { display: block; position: absolute; top: 0; right: 0; left: 0; z-index: 0; overflow: hidden; margin: 0 10px; min-width: 30px; height: 43px; text-align: center; text-overflow: ellipsis; white-space: nowrap; font-size: 17px; font-weight: 500; line-height: 44px; } .bar .title.title-left { text-align: left; } .bar .title.title-right { text-align: right; } .bar .title a { color: inherit; } .bar .button, .bar button { z-index: 1; padding: 0 8px; min-width: initial; min-height: 31px; font-weight: 400; font-size: 13px; line-height: 32px; } .bar .button.button-icon:before, .bar .button .icon:before, .bar .button.icon:before, .bar .button.icon-left:before, .bar .button.icon-right:before, .bar button.button-icon:before, .bar button .icon:before, .bar button.icon:before, .bar button.icon-left:before, .bar button.icon-right:before { padding-right: 2px; padding-left: 2px; font-size: 20px; line-height: 32px; } .bar .button.button-icon, .bar button.button-icon { font-size: 17px; } .bar .button.button-icon .icon:before, .bar .button.button-icon:before, .bar .button.button-icon.icon-left:before, .bar .button.button-icon.icon-right:before, .bar button.button-icon .icon:before, .bar button.button-icon:before, .bar button.button-icon.icon-left:before, .bar button.button-icon.icon-right:before { vertical-align: top; font-size: 32px; line-height: 32px; } .bar .button.button-clear, .bar button.button-clear { padding-right: 2px; padding-left: 2px; font-weight: 300; font-size: 17px; } .bar .button.button-clear .icon:before, .bar .button.button-clear.icon:before, .bar .button.button-clear.icon-left:before, .bar .button.button-clear.icon-right:before, .bar button.button-clear .icon:before, .bar button.button-clear.icon:before, .bar button.button-clear.icon-left:before, .bar button.button-clear.icon-right:before { font-size: 32px; line-height: 32px; } .bar .button.back-button, .bar button.back-button { display: block; margin-right: 5px; padding: 0; white-space: nowrap; font-weight: 400; } .bar .button.back-button.active, .bar .button.back-button.activated, .bar button.back-button.active, .bar button.back-button.activated { opacity: 0.2; } .bar .button-bar > .button, .bar .buttons > .button { min-height: 31px; line-height: 32px; } .bar .button-bar + .button, .bar .button + .button-bar { margin-left: 5px; } .bar .buttons, .bar .buttons.primary-buttons, .bar .buttons.secondary-buttons { display: inherit; } .bar .buttons span { display: inline-block; } .bar .buttons-left span { margin-right: 5px; display: inherit; } .bar .buttons-right span { margin-left: 5px; display: inherit; } .bar .title + .button:last-child, .bar > .button + .button:last-child, .bar > .button.pull-right, .bar .buttons.pull-right, .bar .title + .buttons { position: absolute; top: 5px; right: 5px; bottom: 5px; } .platform-android .nav-bar-has-subheader .bar { background-image: none; } .platform-android .bar .back-button .icon:before { font-size: 24px; } .platform-android .bar .title { font-size: 19px; line-height: 44px; } .bar-light .button { border-color: transparent; background-color: white; color: #444; } .bar-light .button:hover { color: #444; text-decoration: none; } .bar-light .button.active, .bar-light .button.activated { background-color: #fafafa; } .bar-light .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #444; font-size: 17px; } .bar-light .button.button-icon { border-color: transparent; background: none; } .bar-stable .button { border-color: transparent; background-color: #f8f8f8; color: #444; } .bar-stable .button:hover { color: #444; text-decoration: none; } .bar-stable .button.active, .bar-stable .button.activated { background-color: #e5e5e5; } .bar-stable .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #444; font-size: 17px; } .bar-stable .button.button-icon { border-color: transparent; background: none; } .bar-positive .button { border-color: transparent; background-color: #387ef5; color: #fff; } .bar-positive .button:hover { color: #fff; text-decoration: none; } .bar-positive .button.active, .bar-positive .button.activated { background-color: #0c60ee; } .bar-positive .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #fff; font-size: 17px; } .bar-positive .button.button-icon { border-color: transparent; background: none; } .bar-calm .button { border-color: transparent; background-color: #11c1f3; color: #fff; } .bar-calm .button:hover { color: #fff; text-decoration: none; } .bar-calm .button.active, .bar-calm .button.activated { background-color: #0a9dc7; } .bar-calm .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #fff; font-size: 17px; } .bar-calm .button.button-icon { border-color: transparent; background: none; } .bar-assertive .button { border-color: transparent; background-color: #ef473a; color: #fff; } .bar-assertive .button:hover { color: #fff; text-decoration: none; } .bar-assertive .button.active, .bar-assertive .button.activated { background-color: #e42112; } .bar-assertive .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #fff; font-size: 17px; } .bar-assertive .button.button-icon { border-color: transparent; background: none; } .bar-balanced .button { border-color: transparent; background-color: #33cd5f; color: #fff; } .bar-balanced .button:hover { color: #fff; text-decoration: none; } .bar-balanced .button.active, .bar-balanced .button.activated { background-color: #28a54c; } .bar-balanced .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #fff; font-size: 17px; } .bar-balanced .button.button-icon { border-color: transparent; background: none; } .bar-energized .button { border-color: transparent; background-color: #ffc900; color: #fff; } .bar-energized .button:hover { color: #fff; text-decoration: none; } .bar-energized .button.active, .bar-energized .button.activated { background-color: #e6b500; } .bar-energized .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #fff; font-size: 17px; } .bar-energized .button.button-icon { border-color: transparent; background: none; } .bar-royal .button { border-color: transparent; background-color: #886aea; color: #fff; } .bar-royal .button:hover { color: #fff; text-decoration: none; } .bar-royal .button.active, .bar-royal .button.activated { background-color: #6b46e5; } .bar-royal .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #fff; font-size: 17px; } .bar-royal .button.button-icon { border-color: transparent; background: none; } .bar-dark .button { border-color: transparent; background-color: #444444; color: #fff; } .bar-dark .button:hover { color: #fff; text-decoration: none; } .bar-dark .button.active, .bar-dark .button.activated { background-color: #262626; } .bar-dark .button.button-clear { border-color: transparent; background: none; box-shadow: none; color: #fff; font-size: 17px; } .bar-dark .button.button-icon { border-color: transparent; background: none; } .bar-header { top: 0; border-top-width: 0; border-bottom-width: 1px; } .bar-header.has-tabs-top { border-bottom-width: 0px; background-image: none; } .tabs-top .bar-header { border-bottom-width: 0px; background-image: none; } .bar-footer { bottom: 0; border-top-width: 1px; border-bottom-width: 0; background-position: top; height: 44px; } .bar-footer.item-input-inset { position: absolute; } .bar-tabs { padding: 0; } .bar-subheader { top: 44px; display: block; height: 44px; } .bar-subfooter { bottom: 44px; display: block; height: 44px; } .nav-bar-block { position: absolute; top: 0; right: 0; left: 0; z-index: 9; } .bar .back-button.hide, .bar .buttons .hide { display: none; } .nav-bar-tabs-top .bar { background-image: none; } /** * Tabs * -------------------------------------------------- * A navigation bar with any number of tab items supported. */ .tabs { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-direction: normal; -webkit-box-orient: horizontal; -webkit-flex-direction: horizontal; -moz-flex-direction: horizontal; -ms-flex-direction: horizontal; flex-direction: horizontal; -webkit-box-pack: center; -ms-flex-pack: center; -webkit-justify-content: center; -moz-justify-content: center; justify-content: center; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); border-color: #b2b2b2; background-color: #f8f8f8; background-image: linear-gradient(0deg, #b2b2b2, #b2b2b2 50%, transparent 50%); color: #444; position: absolute; bottom: 0; z-index: 5; width: 100%; height: 49px; border-style: solid; border-top-width: 1px; background-size: 0; line-height: 49px; } .tabs .tab-item .badge { background-color: #444; color: #f8f8f8; } @media (min--moz-device-pixel-ratio: 1.5), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 144dpi), (min-resolution: 1.5dppx) { .tabs { padding-top: 2px; border-top: none !important; border-bottom: none; background-position: top; background-size: 100% 1px; background-repeat: no-repeat; } } /* Allow parent element of tabs to define color, or just the tab itself */ .tabs-light > .tabs, .tabs.tabs-light { border-color: #ddd; background-color: #fff; background-image: linear-gradient(0deg, #ddd, #ddd 50%, transparent 50%); color: #444; } .tabs-light > .tabs .tab-item .badge, .tabs.tabs-light .tab-item .badge { background-color: #444; color: #fff; } .tabs-stable > .tabs, .tabs.tabs-stable { border-color: #b2b2b2; background-color: #f8f8f8; background-image: linear-gradient(0deg, #b2b2b2, #b2b2b2 50%, transparent 50%); color: #444; } .tabs-stable > .tabs .tab-item .badge, .tabs.tabs-stable .tab-item .badge { background-color: #444; color: #f8f8f8; } .tabs-positive > .tabs, .tabs.tabs-positive { border-color: #0c60ee; background-color: #387ef5; background-image: linear-gradient(0deg, #0c60ee, #0c60ee 50%, transparent 50%); color: #fff; } .tabs-positive > .tabs .tab-item .badge, .tabs.tabs-positive .tab-item .badge { background-color: #fff; color: #387ef5; } .tabs-calm > .tabs, .tabs.tabs-calm { border-color: #0a9dc7; background-color: #11c1f3; background-image: linear-gradient(0deg, #0a9dc7, #0a9dc7 50%, transparent 50%); color: #fff; } .tabs-calm > .tabs .tab-item .badge, .tabs.tabs-calm .tab-item .badge { background-color: #fff; color: #11c1f3; } .tabs-assertive > .tabs, .tabs.tabs-assertive { border-color: #e42112; background-color: #ef473a; background-image: linear-gradient(0deg, #e42112, #e42112 50%, transparent 50%); color: #fff; } .tabs-assertive > .tabs .tab-item .badge, .tabs.tabs-assertive .tab-item .badge { background-color: #fff; color: #ef473a; } .tabs-balanced > .tabs, .tabs.tabs-balanced { border-color: #28a54c; background-color: #33cd5f; background-image: linear-gradient(0deg, #28a54c, #28a54c 50%, transparent 50%); color: #fff; } .tabs-balanced > .tabs .tab-item .badge, .tabs.tabs-balanced .tab-item .badge { background-color: #fff; color: #33cd5f; } .tabs-energized > .tabs, .tabs.tabs-energized { border-color: #e6b500; background-color: #ffc900; background-image: linear-gradient(0deg, #e6b500, #e6b500 50%, transparent 50%); color: #fff; } .tabs-energized > .tabs .tab-item .badge, .tabs.tabs-energized .tab-item .badge { background-color: #fff; color: #ffc900; } .tabs-royal > .tabs, .tabs.tabs-royal { border-color: #6b46e5; background-color: #886aea; background-image: linear-gradient(0deg, #6b46e5, #6b46e5 50%, transparent 50%); color: #fff; } .tabs-royal > .tabs .tab-item .badge, .tabs.tabs-royal .tab-item .badge { background-color: #fff; color: #886aea; } .tabs-dark > .tabs, .tabs.tabs-dark { border-color: #111; background-color: #444; background-image: linear-gradient(0deg, #111, #111 50%, transparent 50%); color: #fff; } .tabs-dark > .tabs .tab-item .badge, .tabs.tabs-dark .tab-item .badge { background-color: #fff; color: #444; } .tabs-striped .tabs { background-color: white; background-image: none; border: none; border-bottom: 1px solid #ddd; padding-top: 2px; } .tabs-striped .tab-item.tab-item-active, .tabs-striped .tab-item.active, .tabs-striped .tab-item.activated { margin-top: -2px; border-style: solid; border-width: 2px 0 0 0; border-color: #444; } .tabs-striped .tab-item.tab-item-active .badge, .tabs-striped .tab-item.active .badge, .tabs-striped .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-light .tabs { background-color: #fff; } .tabs-striped.tabs-light .tab-item { color: rgba(68, 68, 68, 0.4); opacity: 1; } .tabs-striped.tabs-light .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-light .tab-item.tab-item-active, .tabs-striped.tabs-light .tab-item.active, .tabs-striped.tabs-light .tab-item.activated { margin-top: -2px; color: #444; border-style: solid; border-width: 2px 0 0 0; border-color: #444; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-stable .tabs { background-color: #f8f8f8; } .tabs-striped.tabs-stable .tab-item { color: rgba(68, 68, 68, 0.4); opacity: 1; } .tabs-striped.tabs-stable .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-stable .tab-item.tab-item-active, .tabs-striped.tabs-stable .tab-item.active, .tabs-striped.tabs-stable .tab-item.activated { margin-top: -2px; color: #444; border-style: solid; border-width: 2px 0 0 0; border-color: #444; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-positive .tabs { background-color: #387ef5; } .tabs-striped.tabs-positive .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-striped.tabs-positive .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-positive .tab-item.tab-item-active, .tabs-striped.tabs-positive .tab-item.active, .tabs-striped.tabs-positive .tab-item.activated { margin-top: -2px; color: #fff; border-style: solid; border-width: 2px 0 0 0; border-color: #fff; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-calm .tabs { background-color: #11c1f3; } .tabs-striped.tabs-calm .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-striped.tabs-calm .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-calm .tab-item.tab-item-active, .tabs-striped.tabs-calm .tab-item.active, .tabs-striped.tabs-calm .tab-item.activated { margin-top: -2px; color: #fff; border-style: solid; border-width: 2px 0 0 0; border-color: #fff; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-assertive .tabs { background-color: #ef473a; } .tabs-striped.tabs-assertive .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-striped.tabs-assertive .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-assertive .tab-item.tab-item-active, .tabs-striped.tabs-assertive .tab-item.active, .tabs-striped.tabs-assertive .tab-item.activated { margin-top: -2px; color: #fff; border-style: solid; border-width: 2px 0 0 0; border-color: #fff; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-balanced .tabs { background-color: #33cd5f; } .tabs-striped.tabs-balanced .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-striped.tabs-balanced .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-balanced .tab-item.tab-item-active, .tabs-striped.tabs-balanced .tab-item.active, .tabs-striped.tabs-balanced .tab-item.activated { margin-top: -2px; color: #fff; border-style: solid; border-width: 2px 0 0 0; border-color: #fff; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-energized .tabs { background-color: #ffc900; } .tabs-striped.tabs-energized .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-striped.tabs-energized .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-energized .tab-item.tab-item-active, .tabs-striped.tabs-energized .tab-item.active, .tabs-striped.tabs-energized .tab-item.activated { margin-top: -2px; color: #fff; border-style: solid; border-width: 2px 0 0 0; border-color: #fff; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-royal .tabs { background-color: #886aea; } .tabs-striped.tabs-royal .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-striped.tabs-royal .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-royal .tab-item.tab-item-active, .tabs-striped.tabs-royal .tab-item.active, .tabs-striped.tabs-royal .tab-item.activated { margin-top: -2px; color: #fff; border-style: solid; border-width: 2px 0 0 0; border-color: #fff; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-dark .tabs { background-color: #444; } .tabs-striped.tabs-dark .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-striped.tabs-dark .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-dark .tab-item.tab-item-active, .tabs-striped.tabs-dark .tab-item.active, .tabs-striped.tabs-dark .tab-item.activated { margin-top: -2px; color: #fff; border-style: solid; border-width: 2px 0 0 0; border-color: #fff; } .tabs-striped.tabs-top .tab-item.tab-item-active .badge, .tabs-striped.tabs-top .tab-item.active .badge, .tabs-striped.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-striped.tabs-background-light .tabs { background-color: #fff; background-image: none; } .tabs-striped.tabs-background-stable .tabs { background-color: #f8f8f8; background-image: none; } .tabs-striped.tabs-background-positive .tabs { background-color: #387ef5; background-image: none; } .tabs-striped.tabs-background-calm .tabs { background-color: #11c1f3; background-image: none; } .tabs-striped.tabs-background-assertive .tabs { background-color: #ef473a; background-image: none; } .tabs-striped.tabs-background-balanced .tabs { background-color: #33cd5f; background-image: none; } .tabs-striped.tabs-background-energized .tabs { background-color: #ffc900; background-image: none; } .tabs-striped.tabs-background-royal .tabs { background-color: #886aea; background-image: none; } .tabs-striped.tabs-background-dark .tabs { background-color: #444; background-image: none; } .tabs-striped.tabs-color-light .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-striped.tabs-color-light .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-light .tab-item.tab-item-active, .tabs-striped.tabs-color-light .tab-item.active, .tabs-striped.tabs-color-light .tab-item.activated { margin-top: -2px; color: #fff; border: 0 solid #fff; border-top-width: 2px; } .tabs-striped.tabs-color-light .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-light .tab-item.active .badge, .tabs-striped.tabs-color-light .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-color-stable .tab-item { color: rgba(248, 248, 248, 0.4); opacity: 1; } .tabs-striped.tabs-color-stable .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-stable .tab-item.tab-item-active, .tabs-striped.tabs-color-stable .tab-item.active, .tabs-striped.tabs-color-stable .tab-item.activated { margin-top: -2px; color: #f8f8f8; border: 0 solid #f8f8f8; border-top-width: 2px; } .tabs-striped.tabs-color-stable .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-stable .tab-item.active .badge, .tabs-striped.tabs-color-stable .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-color-positive .tab-item { color: rgba(56, 126, 245, 0.4); opacity: 1; } .tabs-striped.tabs-color-positive .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-positive .tab-item.tab-item-active, .tabs-striped.tabs-color-positive .tab-item.active, .tabs-striped.tabs-color-positive .tab-item.activated { margin-top: -2px; color: #387ef5; border: 0 solid #387ef5; border-top-width: 2px; } .tabs-striped.tabs-color-positive .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-positive .tab-item.active .badge, .tabs-striped.tabs-color-positive .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-color-calm .tab-item { color: rgba(17, 193, 243, 0.4); opacity: 1; } .tabs-striped.tabs-color-calm .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-calm .tab-item.tab-item-active, .tabs-striped.tabs-color-calm .tab-item.active, .tabs-striped.tabs-color-calm .tab-item.activated { margin-top: -2px; color: #11c1f3; border: 0 solid #11c1f3; border-top-width: 2px; } .tabs-striped.tabs-color-calm .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-calm .tab-item.active .badge, .tabs-striped.tabs-color-calm .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-color-assertive .tab-item { color: rgba(239, 71, 58, 0.4); opacity: 1; } .tabs-striped.tabs-color-assertive .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-assertive .tab-item.tab-item-active, .tabs-striped.tabs-color-assertive .tab-item.active, .tabs-striped.tabs-color-assertive .tab-item.activated { margin-top: -2px; color: #ef473a; border: 0 solid #ef473a; border-top-width: 2px; } .tabs-striped.tabs-color-assertive .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-assertive .tab-item.active .badge, .tabs-striped.tabs-color-assertive .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-color-balanced .tab-item { color: rgba(51, 205, 95, 0.4); opacity: 1; } .tabs-striped.tabs-color-balanced .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-balanced .tab-item.tab-item-active, .tabs-striped.tabs-color-balanced .tab-item.active, .tabs-striped.tabs-color-balanced .tab-item.activated { margin-top: -2px; color: #33cd5f; border: 0 solid #33cd5f; border-top-width: 2px; } .tabs-striped.tabs-color-balanced .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-balanced .tab-item.active .badge, .tabs-striped.tabs-color-balanced .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-color-energized .tab-item { color: rgba(255, 201, 0, 0.4); opacity: 1; } .tabs-striped.tabs-color-energized .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-energized .tab-item.tab-item-active, .tabs-striped.tabs-color-energized .tab-item.active, .tabs-striped.tabs-color-energized .tab-item.activated { margin-top: -2px; color: #ffc900; border: 0 solid #ffc900; border-top-width: 2px; } .tabs-striped.tabs-color-energized .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-energized .tab-item.active .badge, .tabs-striped.tabs-color-energized .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-color-royal .tab-item { color: rgba(136, 106, 234, 0.4); opacity: 1; } .tabs-striped.tabs-color-royal .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-royal .tab-item.tab-item-active, .tabs-striped.tabs-color-royal .tab-item.active, .tabs-striped.tabs-color-royal .tab-item.activated { margin-top: -2px; color: #886aea; border: 0 solid #886aea; border-top-width: 2px; } .tabs-striped.tabs-color-royal .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-royal .tab-item.active .badge, .tabs-striped.tabs-color-royal .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-striped.tabs-color-dark .tab-item { color: rgba(68, 68, 68, 0.4); opacity: 1; } .tabs-striped.tabs-color-dark .tab-item .badge { opacity: 0.4; } .tabs-striped.tabs-color-dark .tab-item.tab-item-active, .tabs-striped.tabs-color-dark .tab-item.active, .tabs-striped.tabs-color-dark .tab-item.activated { margin-top: -2px; color: #444; border: 0 solid #444; border-top-width: 2px; } .tabs-striped.tabs-color-dark .tab-item.tab-item-active .badge, .tabs-striped.tabs-color-dark .tab-item.active .badge, .tabs-striped.tabs-color-dark .tab-item.activated .badge { top: 2px; opacity: 1; } .tabs-background-light .tabs, .tabs-background-light > .tabs { background-color: #fff; background-image: linear-gradient(0deg, #ddd, #ddd 50%, transparent 50%); border-color: #ddd; } .tabs-background-stable .tabs, .tabs-background-stable > .tabs { background-color: #f8f8f8; background-image: linear-gradient(0deg, #b2b2b2, #b2b2b2 50%, transparent 50%); border-color: #b2b2b2; } .tabs-background-positive .tabs, .tabs-background-positive > .tabs { background-color: #387ef5; background-image: linear-gradient(0deg, #0c60ee, #0c60ee 50%, transparent 50%); border-color: #0c60ee; } .tabs-background-calm .tabs, .tabs-background-calm > .tabs { background-color: #11c1f3; background-image: linear-gradient(0deg, #0a9dc7, #0a9dc7 50%, transparent 50%); border-color: #0a9dc7; } .tabs-background-assertive .tabs, .tabs-background-assertive > .tabs { background-color: #ef473a; background-image: linear-gradient(0deg, #e42112, #e42112 50%, transparent 50%); border-color: #e42112; } .tabs-background-balanced .tabs, .tabs-background-balanced > .tabs { background-color: #33cd5f; background-image: linear-gradient(0deg, #28a54c, #28a54c 50%, transparent 50%); border-color: #28a54c; } .tabs-background-energized .tabs, .tabs-background-energized > .tabs { background-color: #ffc900; background-image: linear-gradient(0deg, #e6b500, #e6b500 50%, transparent 50%); border-color: #e6b500; } .tabs-background-royal .tabs, .tabs-background-royal > .tabs { background-color: #886aea; background-image: linear-gradient(0deg, #6b46e5, #6b46e5 50%, transparent 50%); border-color: #6b46e5; } .tabs-background-dark .tabs, .tabs-background-dark > .tabs { background-color: #444; background-image: linear-gradient(0deg, #111, #111 50%, transparent 50%); border-color: #111; } .tabs-color-light .tab-item { color: rgba(255, 255, 255, 0.4); opacity: 1; } .tabs-color-light .tab-item .badge { opacity: 0.4; } .tabs-color-light .tab-item.tab-item-active, .tabs-color-light .tab-item.active, .tabs-color-light .tab-item.activated { color: #fff; border: 0 solid #fff; } .tabs-color-light .tab-item.tab-item-active .badge, .tabs-color-light .tab-item.active .badge, .tabs-color-light .tab-item.activated .badge { opacity: 1; } .tabs-color-stable .tab-item { color: rgba(248, 248, 248, 0.4); opacity: 1; } .tabs-color-stable .tab-item .badge { opacity: 0.4; } .tabs-color-stable .tab-item.tab-item-active, .tabs-color-stable .tab-item.active, .tabs-color-stable .tab-item.activated { color: #f8f8f8; border: 0 solid #f8f8f8; } .tabs-color-stable .tab-item.tab-item-active .badge, .tabs-color-stable .tab-item.active .badge, .tabs-color-stable .tab-item.activated .badge { opacity: 1; } .tabs-color-positive .tab-item { color: rgba(56, 126, 245, 0.4); opacity: 1; } .tabs-color-positive .tab-item .badge { opacity: 0.4; } .tabs-color-positive .tab-item.tab-item-active, .tabs-color-positive .tab-item.active, .tabs-color-positive .tab-item.activated { color: #387ef5; border: 0 solid #387ef5; } .tabs-color-positive .tab-item.tab-item-active .badge, .tabs-color-positive .tab-item.active .badge, .tabs-color-positive .tab-item.activated .badge { opacity: 1; } .tabs-color-calm .tab-item { color: rgba(17, 193, 243, 0.4); opacity: 1; } .tabs-color-calm .tab-item .badge { opacity: 0.4; } .tabs-color-calm .tab-item.tab-item-active, .tabs-color-calm .tab-item.active, .tabs-color-calm .tab-item.activated { color: #11c1f3; border: 0 solid #11c1f3; } .tabs-color-calm .tab-item.tab-item-active .badge, .tabs-color-calm .tab-item.active .badge, .tabs-color-calm .tab-item.activated .badge { opacity: 1; } .tabs-color-assertive .tab-item { color: rgba(239, 71, 58, 0.4); opacity: 1; } .tabs-color-assertive .tab-item .badge { opacity: 0.4; } .tabs-color-assertive .tab-item.tab-item-active, .tabs-color-assertive .tab-item.active, .tabs-color-assertive .tab-item.activated { color: #ef473a; border: 0 solid #ef473a; } .tabs-color-assertive .tab-item.tab-item-active .badge, .tabs-color-assertive .tab-item.active .badge, .tabs-color-assertive .tab-item.activated .badge { opacity: 1; } .tabs-color-balanced .tab-item { color: rgba(51, 205, 95, 0.4); opacity: 1; } .tabs-color-balanced .tab-item .badge { opacity: 0.4; } .tabs-color-balanced .tab-item.tab-item-active, .tabs-color-balanced .tab-item.active, .tabs-color-balanced .tab-item.activated { color: #33cd5f; border: 0 solid #33cd5f; } .tabs-color-balanced .tab-item.tab-item-active .badge, .tabs-color-balanced .tab-item.active .badge, .tabs-color-balanced .tab-item.activated .badge { opacity: 1; } .tabs-color-energized .tab-item { color: rgba(255, 201, 0, 0.4); opacity: 1; } .tabs-color-energized .tab-item .badge { opacity: 0.4; } .tabs-color-energized .tab-item.tab-item-active, .tabs-color-energized .tab-item.active, .tabs-color-energized .tab-item.activated { color: #ffc900; border: 0 solid #ffc900; } .tabs-color-energized .tab-item.tab-item-active .badge, .tabs-color-energized .tab-item.active .badge, .tabs-color-energized .tab-item.activated .badge { opacity: 1; } .tabs-color-royal .tab-item { color: rgba(136, 106, 234, 0.4); opacity: 1; } .tabs-color-royal .tab-item .badge { opacity: 0.4; } .tabs-color-royal .tab-item.tab-item-active, .tabs-color-royal .tab-item.active, .tabs-color-royal .tab-item.activated { color: #886aea; border: 0 solid #886aea; } .tabs-color-royal .tab-item.tab-item-active .badge, .tabs-color-royal .tab-item.active .badge, .tabs-color-royal .tab-item.activated .badge { opacity: 1; } .tabs-color-dark .tab-item { color: rgba(68, 68, 68, 0.4); opacity: 1; } .tabs-color-dark .tab-item .badge { opacity: 0.4; } .tabs-color-dark .tab-item.tab-item-active, .tabs-color-dark .tab-item.active, .tabs-color-dark .tab-item.activated { color: #444; border: 0 solid #444; } .tabs-color-dark .tab-item.tab-item-active .badge, .tabs-color-dark .tab-item.active .badge, .tabs-color-dark .tab-item.activated .badge { opacity: 1; } ion-tabs.tabs-color-active-light .tab-item { color: #444; } ion-tabs.tabs-color-active-light .tab-item.tab-item-active, ion-tabs.tabs-color-active-light .tab-item.active, ion-tabs.tabs-color-active-light .tab-item.activated { color: #fff; } ion-tabs.tabs-color-active-stable .tab-item { color: #444; } ion-tabs.tabs-color-active-stable .tab-item.tab-item-active, ion-tabs.tabs-color-active-stable .tab-item.active, ion-tabs.tabs-color-active-stable .tab-item.activated { color: #f8f8f8; } ion-tabs.tabs-color-active-positive .tab-item { color: #444; } ion-tabs.tabs-color-active-positive .tab-item.tab-item-active, ion-tabs.tabs-color-active-positive .tab-item.active, ion-tabs.tabs-color-active-positive .tab-item.activated { color: #387ef5; } ion-tabs.tabs-color-active-calm .tab-item { color: #444; } ion-tabs.tabs-color-active-calm .tab-item.tab-item-active, ion-tabs.tabs-color-active-calm .tab-item.active, ion-tabs.tabs-color-active-calm .tab-item.activated { color: #11c1f3; } ion-tabs.tabs-color-active-assertive .tab-item { color: #444; } ion-tabs.tabs-color-active-assertive .tab-item.tab-item-active, ion-tabs.tabs-color-active-assertive .tab-item.active, ion-tabs.tabs-color-active-assertive .tab-item.activated { color: #ef473a; } ion-tabs.tabs-color-active-balanced .tab-item { color: #444; } ion-tabs.tabs-color-active-balanced .tab-item.tab-item-active, ion-tabs.tabs-color-active-balanced .tab-item.active, ion-tabs.tabs-color-active-balanced .tab-item.activated { color: #33cd5f; } ion-tabs.tabs-color-active-energized .tab-item { color: #444; } ion-tabs.tabs-color-active-energized .tab-item.tab-item-active, ion-tabs.tabs-color-active-energized .tab-item.active, ion-tabs.tabs-color-active-energized .tab-item.activated { color: #ffc900; } ion-tabs.tabs-color-active-royal .tab-item { color: #444; } ion-tabs.tabs-color-active-royal .tab-item.tab-item-active, ion-tabs.tabs-color-active-royal .tab-item.active, ion-tabs.tabs-color-active-royal .tab-item.activated { color: #886aea; } ion-tabs.tabs-color-active-dark .tab-item { color: #fff; } ion-tabs.tabs-color-active-dark .tab-item.tab-item-active, ion-tabs.tabs-color-active-dark .tab-item.active, ion-tabs.tabs-color-active-dark .tab-item.activated { color: #444; } .tabs-top.tabs-striped { padding-bottom: 0; } .tabs-top.tabs-striped .tab-item { background: transparent; -webkit-transition: color .1s ease; -moz-transition: color .1s ease; -ms-transition: color .1s ease; -o-transition: color .1s ease; transition: color .1s ease; } .tabs-top.tabs-striped .tab-item.tab-item-active, .tabs-top.tabs-striped .tab-item.active, .tabs-top.tabs-striped .tab-item.activated { margin-top: 1px; border-width: 0px 0px 2px 0px !important; border-style: solid; } .tabs-top.tabs-striped .tab-item.tab-item-active > .badge, .tabs-top.tabs-striped .tab-item.tab-item-active > i, .tabs-top.tabs-striped .tab-item.active > .badge, .tabs-top.tabs-striped .tab-item.active > i, .tabs-top.tabs-striped .tab-item.activated > .badge, .tabs-top.tabs-striped .tab-item.activated > i { margin-top: -1px; } .tabs-top.tabs-striped .tab-item .badge { -webkit-transition: color .2s ease; -moz-transition: color .2s ease; -ms-transition: color .2s ease; -o-transition: color .2s ease; transition: color .2s ease; } .tabs-top.tabs-striped:not(.tabs-icon-left):not(.tabs-icon-top) .tab-item.tab-item-active .tab-title, .tabs-top.tabs-striped:not(.tabs-icon-left):not(.tabs-icon-top) .tab-item.tab-item-active i, .tabs-top.tabs-striped:not(.tabs-icon-left):not(.tabs-icon-top) .tab-item.active .tab-title, .tabs-top.tabs-striped:not(.tabs-icon-left):not(.tabs-icon-top) .tab-item.active i, .tabs-top.tabs-striped:not(.tabs-icon-left):not(.tabs-icon-top) .tab-item.activated .tab-title, .tabs-top.tabs-striped:not(.tabs-icon-left):not(.tabs-icon-top) .tab-item.activated i { display: block; margin-top: -1px; } .tabs-top.tabs-striped.tabs-icon-left .tab-item { margin-top: 1px; } .tabs-top.tabs-striped.tabs-icon-left .tab-item.tab-item-active .tab-title, .tabs-top.tabs-striped.tabs-icon-left .tab-item.tab-item-active i, .tabs-top.tabs-striped.tabs-icon-left .tab-item.active .tab-title, .tabs-top.tabs-striped.tabs-icon-left .tab-item.active i, .tabs-top.tabs-striped.tabs-icon-left .tab-item.activated .tab-title, .tabs-top.tabs-striped.tabs-icon-left .tab-item.activated i { margin-top: -0.1em; } /* Allow parent element to have tabs-top */ /* If you change this, change platform.scss as well */ .tabs-top > .tabs, .tabs.tabs-top { top: 44px; padding-top: 0; background-position: bottom; border-top-width: 0; border-bottom-width: 1px; } .tabs-top > .tabs .tab-item.tab-item-active .badge, .tabs-top > .tabs .tab-item.active .badge, .tabs-top > .tabs .tab-item.activated .badge, .tabs.tabs-top .tab-item.tab-item-active .badge, .tabs.tabs-top .tab-item.active .badge, .tabs.tabs-top .tab-item.activated .badge { top: 4%; } .tabs-top ~ .bar-header { border-bottom-width: 0; } .tab-item { -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; display: block; overflow: hidden; max-width: 150px; height: 100%; color: inherit; text-align: center; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; font-size: 14px; font-family: "-apple-system", "Helvetica Neue", "Roboto", "Segoe UI", sans-serif; opacity: 0.7; } .tab-item:hover { cursor: pointer; } .tab-item.tab-hidden { display: none; } .tabs-item-hide > .tabs, .tabs.tabs-item-hide { display: none; } .tabs-icon-top > .tabs .tab-item, .tabs-icon-top.tabs .tab-item, .tabs-icon-bottom > .tabs .tab-item, .tabs-icon-bottom.tabs .tab-item { font-size: 10px; line-height: 14px; } .tab-item .icon { display: block; margin: 0 auto; height: 32px; font-size: 32px; } .tabs-icon-left.tabs .tab-item, .tabs-icon-left > .tabs .tab-item, .tabs-icon-right.tabs .tab-item, .tabs-icon-right > .tabs .tab-item { font-size: 10px; } .tabs-icon-left.tabs .tab-item .icon, .tabs-icon-left.tabs .tab-item .tab-title, .tabs-icon-left > .tabs .tab-item .icon, .tabs-icon-left > .tabs .tab-item .tab-title, .tabs-icon-right.tabs .tab-item .icon, .tabs-icon-right.tabs .tab-item .tab-title, .tabs-icon-right > .tabs .tab-item .icon, .tabs-icon-right > .tabs .tab-item .tab-title { display: inline-block; vertical-align: top; margin-top: -.1em; } .tabs-icon-left.tabs .tab-item .icon:before, .tabs-icon-left.tabs .tab-item .tab-title:before, .tabs-icon-left > .tabs .tab-item .icon:before, .tabs-icon-left > .tabs .tab-item .tab-title:before, .tabs-icon-right.tabs .tab-item .icon:before, .tabs-icon-right.tabs .tab-item .tab-title:before, .tabs-icon-right > .tabs .tab-item .icon:before, .tabs-icon-right > .tabs .tab-item .tab-title:before { font-size: 24px; line-height: 49px; } .tabs-icon-left > .tabs .tab-item .icon, .tabs-icon-left.tabs .tab-item .icon { padding-right: 3px; } .tabs-icon-right > .tabs .tab-item .icon, .tabs-icon-right.tabs .tab-item .icon { padding-left: 3px; } .tabs-icon-only > .tabs .icon, .tabs-icon-only.tabs .icon { line-height: inherit; } .tab-item.has-badge { position: relative; } .tab-item .badge { position: absolute; top: 4%; right: 33%; right: calc(50% - 26px); padding: 1px 6px; height: auto; font-size: 12px; line-height: 16px; } /* Navigational tab */ /* Active state for tab */ .tab-item.tab-item-active, .tab-item.active, .tab-item.activated { opacity: 1; } .tab-item.tab-item-active.tab-item-light, .tab-item.active.tab-item-light, .tab-item.activated.tab-item-light { color: #fff; } .tab-item.tab-item-active.tab-item-stable, .tab-item.active.tab-item-stable, .tab-item.activated.tab-item-stable { color: #f8f8f8; } .tab-item.tab-item-active.tab-item-positive, .tab-item.active.tab-item-positive, .tab-item.activated.tab-item-positive { color: #387ef5; } .tab-item.tab-item-active.tab-item-calm, .tab-item.active.tab-item-calm, .tab-item.activated.tab-item-calm { color: #11c1f3; } .tab-item.tab-item-active.tab-item-assertive, .tab-item.active.tab-item-assertive, .tab-item.activated.tab-item-assertive { color: #ef473a; } .tab-item.tab-item-active.tab-item-balanced, .tab-item.active.tab-item-balanced, .tab-item.activated.tab-item-balanced { color: #33cd5f; } .tab-item.tab-item-active.tab-item-energized, .tab-item.active.tab-item-energized, .tab-item.activated.tab-item-energized { color: #ffc900; } .tab-item.tab-item-active.tab-item-royal, .tab-item.active.tab-item-royal, .tab-item.activated.tab-item-royal { color: #886aea; } .tab-item.tab-item-active.tab-item-dark, .tab-item.active.tab-item-dark, .tab-item.activated.tab-item-dark { color: #444; } .item.tabs { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; padding: 0; } .item.tabs .icon:before { position: relative; } .tab-item.disabled, .tab-item[disabled] { opacity: .4; cursor: default; pointer-events: none; } .nav-bar-tabs-top.hide ~ .view-container .tabs-top .tabs { top: 0; } .pane[hide-nav-bar="true"] .has-tabs-top { top: 49px; } /** * Menus * -------------------------------------------------- * Side panel structure */ .menu { position: absolute; top: 0; bottom: 0; z-index: 0; overflow: hidden; min-height: 100%; max-height: 100%; width: 275px; background-color: #fff; } .menu .scroll-content { z-index: 10; } .menu .bar-header { z-index: 11; } .menu-content { -webkit-transform: none; transform: none; box-shadow: -1px 0px 2px rgba(0, 0, 0, 0.2), 1px 0px 2px rgba(0, 0, 0, 0.2); } .menu-open .menu-content .pane, .menu-open .menu-content .scroll-content { pointer-events: none; } .menu-open .menu-content .scroll-content .scroll { pointer-events: none; } .menu-open .menu-content .scroll-content:not(.overflow-scroll) { overflow: hidden; } .grade-b .menu-content, .grade-c .menu-content { -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; right: -1px; left: -1px; border-right: 1px solid #ccc; border-left: 1px solid #ccc; box-shadow: none; } .menu-left { left: 0; } .menu-right { right: 0; } .aside-open.aside-resizing .menu-right { display: none; } .menu-animated { -webkit-transition: -webkit-transform 200ms ease; transition: transform 200ms ease; } /** * Modals * -------------------------------------------------- * Modals are independent windows that slide in from off-screen. */ .modal-backdrop, .modal-backdrop-bg { position: fixed; top: 0; left: 0; z-index: 10; width: 100%; height: 100%; } .modal-backdrop-bg { pointer-events: none; } .modal { display: block; position: absolute; top: 0; z-index: 10; overflow: hidden; min-height: 100%; width: 100%; background-color: #fff; } @media (min-width: 680px) { .modal { top: 20%; right: 20%; bottom: 20%; left: 20%; min-height: 240px; width: 60%; } .modal.ng-leave-active { bottom: 0; } .platform-ios.platform-cordova .modal-wrapper .modal .bar-header:not(.bar-subheader) { height: 44px; } .platform-ios.platform-cordova .modal-wrapper .modal .bar-header:not(.bar-subheader) > * { margin-top: 0; } .platform-ios.platform-cordova .modal-wrapper .modal .tabs-top > .tabs, .platform-ios.platform-cordova .modal-wrapper .modal .tabs.tabs-top { top: 44px; } .platform-ios.platform-cordova .modal-wrapper .modal .has-header, .platform-ios.platform-cordova .modal-wrapper .modal .bar-subheader { top: 44px; } .platform-ios.platform-cordova .modal-wrapper .modal .has-subheader { top: 88px; } .platform-ios.platform-cordova .modal-wrapper .modal .has-header.has-tabs-top { top: 93px; } .platform-ios.platform-cordova .modal-wrapper .modal .has-header.has-subheader.has-tabs-top { top: 137px; } .modal-backdrop-bg { -webkit-transition: opacity 300ms ease-in-out; transition: opacity 300ms ease-in-out; background-color: #000; opacity: 0; } .active .modal-backdrop-bg { opacity: 0.5; } } .modal-open { pointer-events: none; } .modal-open .modal, .modal-open .modal-backdrop { pointer-events: auto; } .modal-open.loading-active .modal, .modal-open.loading-active .modal-backdrop { pointer-events: none; } /** * Popovers * -------------------------------------------------- * Popovers are independent views which float over content */ .popover-backdrop { position: fixed; top: 0; left: 0; z-index: 10; width: 100%; height: 100%; background-color: transparent; } .popover-backdrop.active { background-color: rgba(0, 0, 0, 0.1); } .popover { position: absolute; top: 25%; left: 50%; z-index: 10; display: block; margin-top: 12px; margin-left: -110px; height: 280px; width: 220px; background-color: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); opacity: 0; } .popover .item:first-child { border-top: 0; } .popover .item:last-child { border-bottom: 0; } .popover.popover-bottom { margin-top: -12px; } .popover, .popover .bar-header { border-radius: 2px; } .popover .scroll-content { z-index: 1; margin: 2px 0; } .popover .bar-header { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .popover .has-header { border-top-right-radius: 0; border-top-left-radius: 0; } .popover-arrow { display: none; } .platform-ios .popover { box-shadow: 0 0 40px rgba(0, 0, 0, 0.08); border-radius: 10px; } .platform-ios .popover .bar-header { -webkit-border-top-right-radius: 10px; border-top-right-radius: 10px; -webkit-border-top-left-radius: 10px; border-top-left-radius: 10px; } .platform-ios .popover .scroll-content { margin: 8px 0; border-radius: 10px; } .platform-ios .popover .scroll-content.has-header { margin-top: 0; } .platform-ios .popover-arrow { position: absolute; display: block; top: -17px; width: 30px; height: 19px; overflow: hidden; } .platform-ios .popover-arrow:after { position: absolute; top: 12px; left: 5px; width: 20px; height: 20px; background-color: #fff; border-radius: 3px; content: ''; -webkit-transform: rotate(-45deg); transform: rotate(-45deg); } .platform-ios .popover-bottom .popover-arrow { top: auto; bottom: -10px; } .platform-ios .popover-bottom .popover-arrow:after { top: -6px; } .platform-android .popover { margin-top: -32px; background-color: #fafafa; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); } .platform-android .popover .item { border-color: #fafafa; background-color: #fafafa; color: #4d4d4d; } .platform-android .popover.popover-bottom { margin-top: 32px; } .platform-android .popover-backdrop, .platform-android .popover-backdrop.active { background-color: transparent; } .popover-open { pointer-events: none; } .popover-open .popover, .popover-open .popover-backdrop { pointer-events: auto; } .popover-open.loading-active .popover, .popover-open.loading-active .popover-backdrop { pointer-events: none; } @media (min-width: 680px) { .popover { width: 360px; margin-left: -180px; } } /** * Popups * -------------------------------------------------- */ .popup-container { position: absolute; top: 0; left: 0; bottom: 0; right: 0; background: transparent; display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-pack: center; -ms-flex-pack: center; -webkit-justify-content: center; -moz-justify-content: center; justify-content: center; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; z-index: 12; visibility: hidden; } .popup-container.popup-showing { visibility: visible; } .popup-container.popup-hidden .popup { -webkit-animation-name: scaleOut; animation-name: scaleOut; -webkit-animation-duration: 0.1s; animation-duration: 0.1s; -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; -webkit-animation-fill-mode: both; animation-fill-mode: both; } .popup-container.active .popup { -webkit-animation-name: superScaleIn; animation-name: superScaleIn; -webkit-animation-duration: 0.2s; animation-duration: 0.2s; -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; -webkit-animation-fill-mode: both; animation-fill-mode: both; } .popup-container .popup { width: 250px; max-width: 100%; max-height: 90%; border-radius: 0px; background-color: rgba(255, 255, 255, 0.9); display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-direction: normal; -webkit-box-orient: vertical; -webkit-flex-direction: column; -moz-flex-direction: column; -ms-flex-direction: column; flex-direction: column; } .popup-container input, .popup-container textarea { width: 100%; } .popup-head { padding: 15px 10px; border-bottom: 1px solid #eee; text-align: center; } .popup-title { margin: 0; padding: 0; font-size: 15px; } .popup-sub-title { margin: 5px 0 0 0; padding: 0; font-weight: normal; font-size: 11px; } .popup-body { padding: 10px; overflow: auto; } .popup-buttons { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-direction: normal; -webkit-box-orient: horizontal; -webkit-flex-direction: row; -moz-flex-direction: row; -ms-flex-direction: row; flex-direction: row; padding: 10px; min-height: 65px; } .popup-buttons .button { -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; display: block; min-height: 45px; border-radius: 2px; line-height: 20px; margin-right: 5px; } .popup-buttons .button:last-child { margin-right: 0px; } .popup-open { pointer-events: none; } .popup-open.modal-open .modal { pointer-events: none; } .popup-open .popup-backdrop, .popup-open .popup { pointer-events: auto; } /** * Loading * -------------------------------------------------- */ .loading-container { position: absolute; left: 0; top: 0; right: 0; bottom: 0; z-index: 13; display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-pack: center; -ms-flex-pack: center; -webkit-justify-content: center; -moz-justify-content: center; justify-content: center; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; -webkit-transition: 0.2s opacity linear; transition: 0.2s opacity linear; visibility: hidden; opacity: 0; } .loading-container:not(.visible) .icon, .loading-container:not(.visible) .spinner { display: none; } .loading-container.visible { visibility: visible; } .loading-container.active { opacity: 1; } .loading-container .loading { padding: 20px; border-radius: 5px; background-color: rgba(0, 0, 0, 0.7); color: #fff; text-align: center; text-overflow: ellipsis; font-size: 15px; } .loading-container .loading h1, .loading-container .loading h2, .loading-container .loading h3, .loading-container .loading h4, .loading-container .loading h5, .loading-container .loading h6 { color: #fff; } /** * Items * -------------------------------------------------- */ .item { border-color: #ddd; background-color: #fff; color: #444; position: relative; z-index: 2; display: block; margin: -1px; padding: 16px; border-width: 1px; border-style: solid; font-size: 16px; } .item h2 { margin: 0 0 2px 0; font-size: 16px; font-weight: normal; } .item h3 { margin: 0 0 4px 0; font-size: 14px; } .item h4 { margin: 0 0 4px 0; font-size: 12px; } .item h5, .item h6 { margin: 0 0 3px 0; font-size: 10px; } .item p { color: #666; font-size: 14px; margin-bottom: 2px; } .item h1:last-child, .item h2:last-child, .item h3:last-child, .item h4:last-child, .item h5:last-child, .item h6:last-child, .item p:last-child { margin-bottom: 0; } .item .badge { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; position: absolute; top: 16px; right: 32px; } .item.item-button-right .badge { right: 67px; } .item.item-divider .badge { top: 8px; } .item .badge + .badge { margin-right: 5px; } .item.item-light { border-color: #ddd; background-color: #fff; color: #444; } .item.item-stable { border-color: #b2b2b2; background-color: #f8f8f8; color: #444; } .item.item-positive { border-color: #0c60ee; background-color: #387ef5; color: #fff; } .item.item-calm { border-color: #0a9dc7; background-color: #11c1f3; color: #fff; } .item.item-assertive { border-color: #e42112; background-color: #ef473a; color: #fff; } .item.item-balanced { border-color: #28a54c; background-color: #33cd5f; color: #fff; } .item.item-energized { border-color: #e6b500; background-color: #ffc900; color: #fff; } .item.item-royal { border-color: #6b46e5; background-color: #886aea; color: #fff; } .item.item-dark { border-color: #111; background-color: #444; color: #fff; } .item[ng-click]:hover { cursor: pointer; } .list-borderless .item, .item-borderless { border-width: 0; } .item.active, .item.activated, .item-complex.active .item-content, .item-complex.activated .item-content, .item .item-content.active, .item .item-content.activated { border-color: #ccc; background-color: #D9D9D9; } .item.active.item-complex > .item-content, .item.activated.item-complex > .item-content, .item-complex.active .item-content.item-complex > .item-content, .item-complex.activated .item-content.item-complex > .item-content, .item .item-content.active.item-complex > .item-content, .item .item-content.activated.item-complex > .item-content { border-color: #ccc; background-color: #D9D9D9; } .item.active.item-light, .item.activated.item-light, .item-complex.active .item-content.item-light, .item-complex.activated .item-content.item-light, .item .item-content.active.item-light, .item .item-content.activated.item-light { border-color: #ccc; background-color: #fafafa; } .item.active.item-light.item-complex > .item-content, .item.activated.item-light.item-complex > .item-content, .item-complex.active .item-content.item-light.item-complex > .item-content, .item-complex.activated .item-content.item-light.item-complex > .item-content, .item .item-content.active.item-light.item-complex > .item-content, .item .item-content.activated.item-light.item-complex > .item-content { border-color: #ccc; background-color: #fafafa; } .item.active.item-stable, .item.activated.item-stable, .item-complex.active .item-content.item-stable, .item-complex.activated .item-content.item-stable, .item .item-content.active.item-stable, .item .item-content.activated.item-stable { border-color: #a2a2a2; background-color: #e5e5e5; } .item.active.item-stable.item-complex > .item-content, .item.activated.item-stable.item-complex > .item-content, .item-complex.active .item-content.item-stable.item-complex > .item-content, .item-complex.activated .item-content.item-stable.item-complex > .item-content, .item .item-content.active.item-stable.item-complex > .item-content, .item .item-content.activated.item-stable.item-complex > .item-content { border-color: #a2a2a2; background-color: #e5e5e5; } .item.active.item-positive, .item.activated.item-positive, .item-complex.active .item-content.item-positive, .item-complex.activated .item-content.item-positive, .item .item-content.active.item-positive, .item .item-content.activated.item-positive { border-color: #0c60ee; background-color: #0c60ee; } .item.active.item-positive.item-complex > .item-content, .item.activated.item-positive.item-complex > .item-content, .item-complex.active .item-content.item-positive.item-complex > .item-content, .item-complex.activated .item-content.item-positive.item-complex > .item-content, .item .item-content.active.item-positive.item-complex > .item-content, .item .item-content.activated.item-positive.item-complex > .item-content { border-color: #0c60ee; background-color: #0c60ee; } .item.active.item-calm, .item.activated.item-calm, .item-complex.active .item-content.item-calm, .item-complex.activated .item-content.item-calm, .item .item-content.active.item-calm, .item .item-content.activated.item-calm { border-color: #0a9dc7; background-color: #0a9dc7; } .item.active.item-calm.item-complex > .item-content, .item.activated.item-calm.item-complex > .item-content, .item-complex.active .item-content.item-calm.item-complex > .item-content, .item-complex.activated .item-content.item-calm.item-complex > .item-content, .item .item-content.active.item-calm.item-complex > .item-content, .item .item-content.activated.item-calm.item-complex > .item-content { border-color: #0a9dc7; background-color: #0a9dc7; } .item.active.item-assertive, .item.activated.item-assertive, .item-complex.active .item-content.item-assertive, .item-complex.activated .item-content.item-assertive, .item .item-content.active.item-assertive, .item .item-content.activated.item-assertive { border-color: #e42112; background-color: #e42112; } .item.active.item-assertive.item-complex > .item-content, .item.activated.item-assertive.item-complex > .item-content, .item-complex.active .item-content.item-assertive.item-complex > .item-content, .item-complex.activated .item-content.item-assertive.item-complex > .item-content, .item .item-content.active.item-assertive.item-complex > .item-content, .item .item-content.activated.item-assertive.item-complex > .item-content { border-color: #e42112; background-color: #e42112; } .item.active.item-balanced, .item.activated.item-balanced, .item-complex.active .item-content.item-balanced, .item-complex.activated .item-content.item-balanced, .item .item-content.active.item-balanced, .item .item-content.activated.item-balanced { border-color: #28a54c; background-color: #28a54c; } .item.active.item-balanced.item-complex > .item-content, .item.activated.item-balanced.item-complex > .item-content, .item-complex.active .item-content.item-balanced.item-complex > .item-content, .item-complex.activated .item-content.item-balanced.item-complex > .item-content, .item .item-content.active.item-balanced.item-complex > .item-content, .item .item-content.activated.item-balanced.item-complex > .item-content { border-color: #28a54c; background-color: #28a54c; } .item.active.item-energized, .item.activated.item-energized, .item-complex.active .item-content.item-energized, .item-complex.activated .item-content.item-energized, .item .item-content.active.item-energized, .item .item-content.activated.item-energized { border-color: #e6b500; background-color: #e6b500; } .item.active.item-energized.item-complex > .item-content, .item.activated.item-energized.item-complex > .item-content, .item-complex.active .item-content.item-energized.item-complex > .item-content, .item-complex.activated .item-content.item-energized.item-complex > .item-content, .item .item-content.active.item-energized.item-complex > .item-content, .item .item-content.activated.item-energized.item-complex > .item-content { border-color: #e6b500; background-color: #e6b500; } .item.active.item-royal, .item.activated.item-royal, .item-complex.active .item-content.item-royal, .item-complex.activated .item-content.item-royal, .item .item-content.active.item-royal, .item .item-content.activated.item-royal { border-color: #6b46e5; background-color: #6b46e5; } .item.active.item-royal.item-complex > .item-content, .item.activated.item-royal.item-complex > .item-content, .item-complex.active .item-content.item-royal.item-complex > .item-content, .item-complex.activated .item-content.item-royal.item-complex > .item-content, .item .item-content.active.item-royal.item-complex > .item-content, .item .item-content.activated.item-royal.item-complex > .item-content { border-color: #6b46e5; background-color: #6b46e5; } .item.active.item-dark, .item.activated.item-dark, .item-complex.active .item-content.item-dark, .item-complex.activated .item-content.item-dark, .item .item-content.active.item-dark, .item .item-content.activated.item-dark { border-color: #000; background-color: #262626; } .item.active.item-dark.item-complex > .item-content, .item.activated.item-dark.item-complex > .item-content, .item-complex.active .item-content.item-dark.item-complex > .item-content, .item-complex.activated .item-content.item-dark.item-complex > .item-content, .item .item-content.active.item-dark.item-complex > .item-content, .item .item-content.activated.item-dark.item-complex > .item-content { border-color: #000; background-color: #262626; } .item, .item h1, .item h2, .item h3, .item h4, .item h5, .item h6, .item p, .item-content, .item-content h1, .item-content h2, .item-content h3, .item-content h4, .item-content h5, .item-content h6, .item-content p { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } a.item { color: inherit; text-decoration: none; } a.item:hover, a.item:focus { text-decoration: none; } /** * Complex Items * -------------------------------------------------- * Adding .item-complex allows the .item to be slidable and * have options underneath the button, but also requires an * additional .item-content element inside .item. * Basically .item-complex removes any default settings which * .item added, so that .item-content looks them as just .item. */ .item-complex, a.item.item-complex, button.item.item-complex { padding: 0; } .item-complex .item-content, .item-radio .item-content { position: relative; z-index: 2; padding: 16px 49px 16px 16px; border: none; background-color: #fff; } a.item-content { display: block; color: inherit; text-decoration: none; } .item-text-wrap .item, .item-text-wrap .item-content, .item-text-wrap, .item-text-wrap h1, .item-text-wrap h2, .item-text-wrap h3, .item-text-wrap h4, .item-text-wrap h5, .item-text-wrap h6, .item-text-wrap p, .item-complex.item-text-wrap .item-content, .item-body h1, .item-body h2, .item-body h3, .item-body h4, .item-body h5, .item-body h6, .item-body p { overflow: visible; white-space: normal; } .item-complex.item-text-wrap, .item-complex.item-text-wrap h1, .item-complex.item-text-wrap h2, .item-complex.item-text-wrap h3, .item-complex.item-text-wrap h4, .item-complex.item-text-wrap h5, .item-complex.item-text-wrap h6, .item-complex.item-text-wrap p { overflow: visible; white-space: normal; } .item-complex.item-light > .item-content { border-color: #ddd; background-color: #fff; color: #444; } .item-complex.item-light > .item-content.active, .item-complex.item-light > .item-content:active { border-color: #ccc; background-color: #fafafa; } .item-complex.item-light > .item-content.active.item-complex > .item-content, .item-complex.item-light > .item-content:active.item-complex > .item-content { border-color: #ccc; background-color: #fafafa; } .item-complex.item-stable > .item-content { border-color: #b2b2b2; background-color: #f8f8f8; color: #444; } .item-complex.item-stable > .item-content.active, .item-complex.item-stable > .item-content:active { border-color: #a2a2a2; background-color: #e5e5e5; } .item-complex.item-stable > .item-content.active.item-complex > .item-content, .item-complex.item-stable > .item-content:active.item-complex > .item-content { border-color: #a2a2a2; background-color: #e5e5e5; } .item-complex.item-positive > .item-content { border-color: #0c60ee; background-color: #387ef5; color: #fff; } .item-complex.item-positive > .item-content.active, .item-complex.item-positive > .item-content:active { border-color: #0c60ee; background-color: #0c60ee; } .item-complex.item-positive > .item-content.active.item-complex > .item-content, .item-complex.item-positive > .item-content:active.item-complex > .item-content { border-color: #0c60ee; background-color: #0c60ee; } .item-complex.item-calm > .item-content { border-color: #0a9dc7; background-color: #11c1f3; color: #fff; } .item-complex.item-calm > .item-content.active, .item-complex.item-calm > .item-content:active { border-color: #0a9dc7; background-color: #0a9dc7; } .item-complex.item-calm > .item-content.active.item-complex > .item-content, .item-complex.item-calm > .item-content:active.item-complex > .item-content { border-color: #0a9dc7; background-color: #0a9dc7; } .item-complex.item-assertive > .item-content { border-color: #e42112; background-color: #ef473a; color: #fff; } .item-complex.item-assertive > .item-content.active, .item-complex.item-assertive > .item-content:active { border-color: #e42112; background-color: #e42112; } .item-complex.item-assertive > .item-content.active.item-complex > .item-content, .item-complex.item-assertive > .item-content:active.item-complex > .item-content { border-color: #e42112; background-color: #e42112; } .item-complex.item-balanced > .item-content { border-color: #28a54c; background-color: #33cd5f; color: #fff; } .item-complex.item-balanced > .item-content.active, .item-complex.item-balanced > .item-content:active { border-color: #28a54c; background-color: #28a54c; } .item-complex.item-balanced > .item-content.active.item-complex > .item-content, .item-complex.item-balanced > .item-content:active.item-complex > .item-content { border-color: #28a54c; background-color: #28a54c; } .item-complex.item-energized > .item-content { border-color: #e6b500; background-color: #ffc900; color: #fff; } .item-complex.item-energized > .item-content.active, .item-complex.item-energized > .item-content:active { border-color: #e6b500; background-color: #e6b500; } .item-complex.item-energized > .item-content.active.item-complex > .item-content, .item-complex.item-energized > .item-content:active.item-complex > .item-content { border-color: #e6b500; background-color: #e6b500; } .item-complex.item-royal > .item-content { border-color: #6b46e5; background-color: #886aea; color: #fff; } .item-complex.item-royal > .item-content.active, .item-complex.item-royal > .item-content:active { border-color: #6b46e5; background-color: #6b46e5; } .item-complex.item-royal > .item-content.active.item-complex > .item-content, .item-complex.item-royal > .item-content:active.item-complex > .item-content { border-color: #6b46e5; background-color: #6b46e5; } .item-complex.item-dark > .item-content { border-color: #111; background-color: #444; color: #fff; } .item-complex.item-dark > .item-content.active, .item-complex.item-dark > .item-content:active { border-color: #000; background-color: #262626; } .item-complex.item-dark > .item-content.active.item-complex > .item-content, .item-complex.item-dark > .item-content:active.item-complex > .item-content { border-color: #000; background-color: #262626; } /** * Item Icons * -------------------------------------------------- */ .item-icon-left .icon, .item-icon-right .icon { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; position: absolute; top: 0; height: 100%; font-size: 32px; } .item-icon-left .icon:before, .item-icon-right .icon:before { display: block; width: 32px; text-align: center; } .item .fill-icon { min-width: 30px; min-height: 30px; font-size: 28px; } .item-icon-left { padding-left: 54px; } .item-icon-left .icon { left: 11px; } .item-complex.item-icon-left { padding-left: 0; } .item-complex.item-icon-left .item-content { padding-left: 54px; } .item-icon-right { padding-right: 54px; } .item-icon-right .icon { right: 11px; } .item-complex.item-icon-right { padding-right: 0; } .item-complex.item-icon-right .item-content { padding-right: 54px; } .item-icon-left.item-icon-right .icon:first-child { right: auto; } .item-icon-left.item-icon-right .icon:last-child, .item-icon-left .item-delete .icon { left: auto; } .item-icon-left .icon-accessory, .item-icon-right .icon-accessory { color: #ccc; font-size: 16px; } .item-icon-left .icon-accessory { left: 3px; } .item-icon-right .icon-accessory { right: 3px; } /** * Item Button * -------------------------------------------------- * An item button is a child button inside an .item (not the entire .item) */ .item-button-left { padding-left: 72px; } .item-button-left > .button, .item-button-left .item-content > .button { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; position: absolute; top: 8px; left: 11px; min-width: 34px; min-height: 34px; font-size: 18px; line-height: 32px; } .item-button-left > .button .icon:before, .item-button-left .item-content > .button .icon:before { position: relative; left: auto; width: auto; line-height: 31px; } .item-button-left > .button > .button, .item-button-left .item-content > .button > .button { margin: 0px 2px; min-height: 34px; font-size: 18px; line-height: 32px; } .item-button-right, a.item.item-button-right, button.item.item-button-right { padding-right: 80px; } .item-button-right > .button, .item-button-right .item-content > .button, .item-button-right > .buttons, .item-button-right .item-content > .buttons { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; position: absolute; top: 8px; right: 16px; min-width: 34px; min-height: 34px; font-size: 18px; line-height: 32px; } .item-button-right > .button .icon:before, .item-button-right .item-content > .button .icon:before, .item-button-right > .buttons .icon:before, .item-button-right .item-content > .buttons .icon:before { position: relative; left: auto; width: auto; line-height: 31px; } .item-button-right > .button > .button, .item-button-right .item-content > .button > .button, .item-button-right > .buttons > .button, .item-button-right .item-content > .buttons > .button { margin: 0px 2px; min-width: 34px; min-height: 34px; font-size: 18px; line-height: 32px; } .item-avatar, .item-avatar .item-content, .item-avatar-left, .item-avatar-left .item-content { padding-left: 72px; min-height: 72px; } .item-avatar > img:first-child, .item-avatar .item-image, .item-avatar .item-content > img:first-child, .item-avatar .item-content .item-image, .item-avatar-left > img:first-child, .item-avatar-left .item-image, .item-avatar-left .item-content > img:first-child, .item-avatar-left .item-content .item-image { position: absolute; top: 16px; left: 16px; max-width: 40px; max-height: 40px; width: 100%; height: 100%; border-radius: 50%; } .item-avatar-right, .item-avatar-right .item-content { padding-right: 72px; min-height: 72px; } .item-avatar-right > img:first-child, .item-avatar-right .item-image, .item-avatar-right .item-content > img:first-child, .item-avatar-right .item-content .item-image { position: absolute; top: 16px; right: 16px; max-width: 40px; max-height: 40px; width: 100%; height: 100%; border-radius: 50%; } .item-thumbnail-left, .item-thumbnail-left .item-content { padding-top: 8px; padding-left: 106px; min-height: 100px; } .item-thumbnail-left > img:first-child, .item-thumbnail-left .item-image, .item-thumbnail-left .item-content > img:first-child, .item-thumbnail-left .item-content .item-image { position: absolute; top: 10px; left: 10px; max-width: 80px; max-height: 80px; width: 100%; height: 100%; } .item-avatar.item-complex, .item-avatar-left.item-complex, .item-thumbnail-left.item-complex { padding-top: 0; padding-left: 0; } .item-thumbnail-right, .item-thumbnail-right .item-content { padding-top: 8px; padding-right: 106px; min-height: 100px; } .item-thumbnail-right > img:first-child, .item-thumbnail-right .item-image, .item-thumbnail-right .item-content > img:first-child, .item-thumbnail-right .item-content .item-image { position: absolute; top: 10px; right: 10px; max-width: 80px; max-height: 80px; width: 100%; height: 100%; } .item-avatar-right.item-complex, .item-thumbnail-right.item-complex { padding-top: 0; padding-right: 0; } .item-image { padding: 0; text-align: center; } .item-image img:first-child, .item-image .list-img { width: 100%; vertical-align: middle; } .item-body { overflow: auto; padding: 16px; text-overflow: inherit; white-space: normal; } .item-body h1, .item-body h2, .item-body h3, .item-body h4, .item-body h5, .item-body h6, .item-body p { margin-top: 16px; margin-bottom: 16px; } .item-divider { padding-top: 8px; padding-bottom: 8px; min-height: 30px; background-color: #f5f5f5; color: #222; font-weight: 500; } .platform-ios .item-divider-platform, .item-divider-ios { padding-top: 26px; text-transform: uppercase; font-weight: 300; font-size: 13px; background-color: #efeff4; color: #555; } .platform-android .item-divider-platform, .item-divider-android { font-weight: 300; font-size: 13px; } .item-note { float: right; color: #aaa; font-size: 14px; } .item-left-editable .item-content, .item-right-editable .item-content { -webkit-transition-duration: 250ms; transition-duration: 250ms; -webkit-transition-timing-function: ease-in-out; transition-timing-function: ease-in-out; -webkit-transition-property: -webkit-transform; -moz-transition-property: -moz-transform; transition-property: transform; } .list-left-editing .item-left-editable .item-content, .item-left-editing.item-left-editable .item-content { -webkit-transform: translate3d(50px, 0, 0); transform: translate3d(50px, 0, 0); } .item-remove-animate.ng-leave { -webkit-transition-duration: 300ms; transition-duration: 300ms; } .item-remove-animate.ng-leave .item-content, .item-remove-animate.ng-leave:last-of-type { -webkit-transition-duration: 300ms; transition-duration: 300ms; -webkit-transition-timing-function: ease-in; transition-timing-function: ease-in; -webkit-transition-property: all; transition-property: all; } .item-remove-animate.ng-leave.ng-leave-active .item-content { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0) !important; transform: translate3d(-100%, 0, 0) !important; } .item-remove-animate.ng-leave.ng-leave-active:last-of-type { opacity: 0; } .item-remove-animate.ng-leave.ng-leave-active ~ ion-item:not(.ng-leave) { -webkit-transform: translate3d(0, -webkit-calc(-100% + 1px), 0); transform: translate3d(0, calc(-100% + 1px), 0); -webkit-transition-duration: 300ms; transition-duration: 300ms; -webkit-transition-timing-function: cubic-bezier(0.25, 0.81, 0.24, 1); transition-timing-function: cubic-bezier(0.25, 0.81, 0.24, 1); -webkit-transition-property: all; transition-property: all; } .item-left-edit { -webkit-transition: all ease-in-out 125ms; transition: all ease-in-out 125ms; position: absolute; top: 0; left: 0; z-index: 0; width: 50px; height: 100%; line-height: 100%; display: none; opacity: 0; -webkit-transform: translate3d(-21px, 0, 0); transform: translate3d(-21px, 0, 0); } .item-left-edit .button { height: 100%; } .item-left-edit .button.icon { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; position: absolute; top: 0; height: 100%; } .item-left-edit.visible { display: block; } .item-left-edit.visible.active { opacity: 1; -webkit-transform: translate3d(8px, 0, 0); transform: translate3d(8px, 0, 0); } .list-left-editing .item-left-edit { -webkit-transition-delay: 125ms; transition-delay: 125ms; } .item-delete .button.icon { color: #ef473a; font-size: 24px; } .item-delete .button.icon:hover { opacity: .7; } .item-right-edit { -webkit-transition: all ease-in-out 250ms; transition: all ease-in-out 250ms; position: absolute; top: 0; right: 0; z-index: 3; width: 75px; height: 100%; background: inherit; padding-left: 20px; display: block; opacity: 0; -webkit-transform: translate3d(75px, 0, 0); transform: translate3d(75px, 0, 0); } .item-right-edit .button { min-width: 50px; height: 100%; } .item-right-edit .button.icon { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; position: absolute; top: 0; height: 100%; font-size: 32px; } .item-right-edit.visible { display: block; } .item-right-edit.visible.active { opacity: 1; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } .item-reorder .button.icon { color: #444; font-size: 32px; } .item-reordering { position: absolute; left: 0; top: 0; z-index: 9; width: 100%; box-shadow: 0px 0px 10px 0px #aaa; } .item-reordering .item-reorder { z-index: 9; } .item-placeholder { opacity: 0.7; } /** * The hidden right-side buttons that can be exposed under a list item * with dragging. */ .item-options { position: absolute; top: 0; right: 0; z-index: 1; height: 100%; } .item-options .button { height: 100%; border: none; border-radius: 0; display: -webkit-inline-box; display: -webkit-inline-flex; display: -moz-inline-flex; display: -ms-inline-flexbox; display: inline-flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; } .item-options .button:before { margin: 0 auto; } /** * Lists * -------------------------------------------------- */ .list { position: relative; padding-top: 1px; padding-bottom: 1px; padding-left: 0; margin-bottom: 20px; } .list:last-child { margin-bottom: 0px; } .list:last-child.card { margin-bottom: 40px; } /** * List Header * -------------------------------------------------- */ .list-header { margin-top: 20px; padding: 5px 15px; background-color: transparent; color: #222; font-weight: bold; } .card.list .list-item { padding-right: 1px; padding-left: 1px; } /** * Cards and Inset Lists * -------------------------------------------------- * A card and list-inset are close to the same thing, except a card as a box shadow. */ .card, .list-inset { overflow: hidden; margin: 20px 10px; border-radius: 2px; background-color: #fff; } .card { padding-top: 1px; padding-bottom: 1px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } .card .item { border-left: 0; border-right: 0; } .card .item:first-child { border-top: 0; } .card .item:last-child { border-bottom: 0; } .padding .card, .padding .list-inset { margin-left: 0; margin-right: 0; } .card .item:first-child, .list-inset .item:first-child, .padding > .list .item:first-child { border-top-left-radius: 2px; border-top-right-radius: 2px; } .card .item:first-child .item-content, .list-inset .item:first-child .item-content, .padding > .list .item:first-child .item-content { border-top-left-radius: 2px; border-top-right-radius: 2px; } .card .item:last-child, .list-inset .item:last-child, .padding > .list .item:last-child { border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; } .card .item:last-child .item-content, .list-inset .item:last-child .item-content, .padding > .list .item:last-child .item-content { border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; } .card .item:last-child, .list-inset .item:last-child { margin-bottom: -1px; } .card .item, .list-inset .item, .padding > .list .item, .padding-horizontal > .list .item { margin-right: 0; margin-left: 0; } .card .item.item-input input, .list-inset .item.item-input input, .padding > .list .item.item-input input, .padding-horizontal > .list .item.item-input input { padding-right: 44px; } .padding-left > .list .item { margin-left: 0; } .padding-right > .list .item { margin-right: 0; } /** * Badges * -------------------------------------------------- */ .badge { background-color: transparent; color: #AAAAAA; z-index: 1; display: inline-block; padding: 3px 8px; min-width: 10px; border-radius: 10px; vertical-align: baseline; text-align: center; white-space: nowrap; font-weight: bold; font-size: 14px; line-height: 16px; } .badge:empty { display: none; } .tabs .tab-item .badge.badge-light, .badge.badge-light { background-color: #fff; color: #444; } .tabs .tab-item .badge.badge-stable, .badge.badge-stable { background-color: #f8f8f8; color: #444; } .tabs .tab-item .badge.badge-positive, .badge.badge-positive { background-color: #387ef5; color: #fff; } .tabs .tab-item .badge.badge-calm, .badge.badge-calm { background-color: #11c1f3; color: #fff; } .tabs .tab-item .badge.badge-assertive, .badge.badge-assertive { background-color: #ef473a; color: #fff; } .tabs .tab-item .badge.badge-balanced, .badge.badge-balanced { background-color: #33cd5f; color: #fff; } .tabs .tab-item .badge.badge-energized, .badge.badge-energized { background-color: #ffc900; color: #fff; } .tabs .tab-item .badge.badge-royal, .badge.badge-royal { background-color: #886aea; color: #fff; } .tabs .tab-item .badge.badge-dark, .badge.badge-dark { background-color: #444; color: #fff; } .button .badge { position: relative; top: -1px; } /** * Slide Box * -------------------------------------------------- */ .slider { position: relative; visibility: hidden; overflow: hidden; } .slider-slides { position: relative; height: 100%; } .slider-slide { position: relative; display: block; float: left; width: 100%; height: 100%; vertical-align: top; } .slider-slide-image > img { width: 100%; } .slider-pager { position: absolute; bottom: 20px; z-index: 1; width: 100%; height: 15px; text-align: center; } .slider-pager .slider-pager-page { display: inline-block; margin: 0px 3px; width: 15px; color: #000; text-decoration: none; opacity: 0.3; } .slider-pager .slider-pager-page.active { -webkit-transition: opacity 0.4s ease-in; transition: opacity 0.4s ease-in; opacity: 1; } .slider-slide.ng-enter, .slider-slide.ng-leave, .slider-slide.ng-animate, .slider-pager-page.ng-enter, .slider-pager-page.ng-leave, .slider-pager-page.ng-animate { -webkit-transition: none !important; transition: none !important; } .slider-slide.ng-animate, .slider-pager-page.ng-animate { -webkit-animation: none 0s; animation: none 0s; } /** * Swiper 3.2.7 * Most modern mobile touch slider and framework with hardware accelerated transitions * * http://www.idangero.us/swiper/ * * Copyright 2015, Vladimir Kharlampidi * The iDangero.us * http://www.idangero.us/ * * Licensed under MIT * * Released on: December 7, 2015 */ .swiper-container { margin: 0 auto; position: relative; overflow: hidden; /* Fix of Webkit flickering */ z-index: 1; } .swiper-container-no-flexbox .swiper-slide { float: left; } .swiper-container-vertical > .swiper-wrapper { -webkit-box-orient: vertical; -moz-box-orient: vertical; -ms-flex-direction: column; -webkit-flex-direction: column; flex-direction: column; } .swiper-wrapper { position: relative; width: 100%; height: 100%; z-index: 1; display: -webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; display: flex; -webkit-transition-property: -webkit-transform; -moz-transition-property: -moz-transform; -o-transition-property: -o-transform; -ms-transition-property: -ms-transform; transition-property: transform; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } .swiper-container-android .swiper-slide, .swiper-wrapper { -webkit-transform: translate3d(0px, 0, 0); -moz-transform: translate3d(0px, 0, 0); -o-transform: translate(0px, 0px); -ms-transform: translate3d(0px, 0, 0); transform: translate3d(0px, 0, 0); } .swiper-container-multirow > .swiper-wrapper { -webkit-box-lines: multiple; -moz-box-lines: multiple; -ms-flex-wrap: wrap; -webkit-flex-wrap: wrap; flex-wrap: wrap; } .swiper-container-free-mode > .swiper-wrapper { -webkit-transition-timing-function: ease-out; -moz-transition-timing-function: ease-out; -ms-transition-timing-function: ease-out; -o-transition-timing-function: ease-out; transition-timing-function: ease-out; margin: 0 auto; } .swiper-slide { display: block; -webkit-flex-shrink: 0; -ms-flex: 0 0 auto; flex-shrink: 0; width: 100%; height: 100%; position: relative; } /* Auto Height */ .swiper-container-autoheight, .swiper-container-autoheight .swiper-slide { height: auto; } .swiper-container-autoheight .swiper-wrapper { -webkit-box-align: start; -ms-flex-align: start; -webkit-align-items: flex-start; align-items: flex-start; -webkit-transition-property: -webkit-transform, height; -moz-transition-property: -moz-transform; -o-transition-property: -o-transform; -ms-transition-property: -ms-transform; transition-property: transform, height; } /* a11y */ .swiper-container .swiper-notification { position: absolute; left: 0; top: 0; pointer-events: none; opacity: 0; z-index: -1000; } /* IE10 Windows Phone 8 Fixes */ .swiper-wp8-horizontal { -ms-touch-action: pan-y; touch-action: pan-y; } .swiper-wp8-vertical { -ms-touch-action: pan-x; touch-action: pan-x; } /* Arrows */ .swiper-button-prev, .swiper-button-next { position: absolute; top: 50%; width: 27px; height: 44px; margin-top: -22px; z-index: 10; cursor: pointer; -moz-background-size: 27px 44px; -webkit-background-size: 27px 44px; background-size: 27px 44px; background-position: center; background-repeat: no-repeat; } .swiper-button-prev.swiper-button-disabled, .swiper-button-next.swiper-button-disabled { opacity: 0.35; cursor: auto; pointer-events: none; } .swiper-button-prev, .swiper-container-rtl .swiper-button-next { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23007aff'%2F%3E%3C%2Fsvg%3E"); left: 10px; right: auto; } .swiper-button-prev.swiper-button-black, .swiper-container-rtl .swiper-button-next.swiper-button-black { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23000000'%2F%3E%3C%2Fsvg%3E"); } .swiper-button-prev.swiper-button-white, .swiper-container-rtl .swiper-button-next.swiper-button-white { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M0%2C22L22%2C0l2.1%2C2.1L4.2%2C22l19.9%2C19.9L22%2C44L0%2C22L0%2C22L0%2C22z'%20fill%3D'%23ffffff'%2F%3E%3C%2Fsvg%3E"); } .swiper-button-next, .swiper-container-rtl .swiper-button-prev { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23007aff'%2F%3E%3C%2Fsvg%3E"); right: 10px; left: auto; } .swiper-button-next.swiper-button-black, .swiper-container-rtl .swiper-button-prev.swiper-button-black { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23000000'%2F%3E%3C%2Fsvg%3E"); } .swiper-button-next.swiper-button-white, .swiper-container-rtl .swiper-button-prev.swiper-button-white { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2027%2044'%3E%3Cpath%20d%3D'M27%2C22L27%2C22L5%2C44l-2.1-2.1L22.8%2C22L2.9%2C2.1L5%2C0L27%2C22L27%2C22z'%20fill%3D'%23ffffff'%2F%3E%3C%2Fsvg%3E"); } /* Pagination Styles */ .swiper-pagination { position: absolute; text-align: center; -webkit-transition: 300ms; -moz-transition: 300ms; -o-transition: 300ms; transition: 300ms; -webkit-transform: translate3d(0, 0, 0); -ms-transform: translate3d(0, 0, 0); -o-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); z-index: 10; } .swiper-pagination.swiper-pagination-hidden { opacity: 0; } .swiper-pagination-bullet { width: 8px; height: 8px; display: inline-block; border-radius: 100%; background: #000; opacity: 0.2; } button.swiper-pagination-bullet { border: none; margin: 0; padding: 0; box-shadow: none; -moz-appearance: none; -ms-appearance: none; -webkit-appearance: none; appearance: none; } .swiper-pagination-clickable .swiper-pagination-bullet { cursor: pointer; } .swiper-pagination-white .swiper-pagination-bullet { background: #fff; } .swiper-pagination-bullet-active { opacity: 1; } .swiper-pagination-white .swiper-pagination-bullet-active { background: #fff; } .swiper-pagination-black .swiper-pagination-bullet-active { background: #000; } .swiper-container-vertical > .swiper-pagination { right: 10px; top: 50%; -webkit-transform: translate3d(0px, -50%, 0); -moz-transform: translate3d(0px, -50%, 0); -o-transform: translate(0px, -50%); -ms-transform: translate3d(0px, -50%, 0); transform: translate3d(0px, -50%, 0); } .swiper-container-vertical > .swiper-pagination .swiper-pagination-bullet { margin: 5px 0; display: block; } .swiper-container-horizontal > .swiper-pagination { bottom: 10px; left: 0; width: 100%; } .swiper-container-horizontal > .swiper-pagination .swiper-pagination-bullet { margin: 0 5px; } /* 3D Container */ .swiper-container-3d { -webkit-perspective: 1200px; -moz-perspective: 1200px; -o-perspective: 1200px; perspective: 1200px; } .swiper-container-3d .swiper-wrapper, .swiper-container-3d .swiper-slide, .swiper-container-3d .swiper-slide-shadow-left, .swiper-container-3d .swiper-slide-shadow-right, .swiper-container-3d .swiper-slide-shadow-top, .swiper-container-3d .swiper-slide-shadow-bottom, .swiper-container-3d .swiper-cube-shadow { -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; -ms-transform-style: preserve-3d; transform-style: preserve-3d; } .swiper-container-3d .swiper-slide-shadow-left, .swiper-container-3d .swiper-slide-shadow-right, .swiper-container-3d .swiper-slide-shadow-top, .swiper-container-3d .swiper-slide-shadow-bottom { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; } .swiper-container-3d .swiper-slide-shadow-left { background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(transparent)); /* Safari 4+, Chrome */ background-image: -webkit-linear-gradient(right, rgba(0, 0, 0, 0.5), transparent); /* Chrome 10+, Safari 5.1+, iOS 5+ */ background-image: -moz-linear-gradient(right, rgba(0, 0, 0, 0.5), transparent); /* Firefox 3.6-15 */ background-image: -o-linear-gradient(right, rgba(0, 0, 0, 0.5), transparent); /* Opera 11.10-12.00 */ background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), transparent); /* Firefox 16+, IE10, Opera 12.50+ */ } .swiper-container-3d .swiper-slide-shadow-right { background-image: -webkit-gradient(linear, right top, left top, from(rgba(0, 0, 0, 0.5)), to(transparent)); /* Safari 4+, Chrome */ background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5), transparent); /* Chrome 10+, Safari 5.1+, iOS 5+ */ background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.5), transparent); /* Firefox 3.6-15 */ background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5), transparent); /* Opera 11.10-12.00 */ background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), transparent); /* Firefox 16+, IE10, Opera 12.50+ */ } .swiper-container-3d .swiper-slide-shadow-top { background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.5)), to(transparent)); /* Safari 4+, Chrome */ background-image: -webkit-linear-gradient(bottom, rgba(0, 0, 0, 0.5), transparent); /* Chrome 10+, Safari 5.1+, iOS 5+ */ background-image: -moz-linear-gradient(bottom, rgba(0, 0, 0, 0.5), transparent); /* Firefox 3.6-15 */ background-image: -o-linear-gradient(bottom, rgba(0, 0, 0, 0.5), transparent); /* Opera 11.10-12.00 */ background-image: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent); /* Firefox 16+, IE10, Opera 12.50+ */ } .swiper-container-3d .swiper-slide-shadow-bottom { background-image: -webkit-gradient(linear, left bottom, left top, from(rgba(0, 0, 0, 0.5)), to(transparent)); /* Safari 4+, Chrome */ background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.5), transparent); /* Chrome 10+, Safari 5.1+, iOS 5+ */ background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0.5), transparent); /* Firefox 3.6-15 */ background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0.5), transparent); /* Opera 11.10-12.00 */ background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent); /* Firefox 16+, IE10, Opera 12.50+ */ } /* Coverflow */ .swiper-container-coverflow .swiper-wrapper { /* Windows 8 IE 10 fix */ -ms-perspective: 1200px; } /* Fade */ .swiper-container-fade.swiper-container-free-mode .swiper-slide { -webkit-transition-timing-function: ease-out; -moz-transition-timing-function: ease-out; -ms-transition-timing-function: ease-out; -o-transition-timing-function: ease-out; transition-timing-function: ease-out; } .swiper-container-fade .swiper-slide { pointer-events: none; } .swiper-container-fade .swiper-slide .swiper-slide { pointer-events: none; } .swiper-container-fade .swiper-slide-active, .swiper-container-fade .swiper-slide-active .swiper-slide-active { pointer-events: auto; } /* Cube */ .swiper-container-cube { overflow: visible; } .swiper-container-cube .swiper-slide { pointer-events: none; visibility: hidden; -webkit-transform-origin: 0 0; -moz-transform-origin: 0 0; -ms-transform-origin: 0 0; transform-origin: 0 0; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden; width: 100%; height: 100%; z-index: 1; } .swiper-container-cube.swiper-container-rtl .swiper-slide { -webkit-transform-origin: 100% 0; -moz-transform-origin: 100% 0; -ms-transform-origin: 100% 0; transform-origin: 100% 0; } .swiper-container-cube .swiper-slide-active, .swiper-container-cube .swiper-slide-next, .swiper-container-cube .swiper-slide-prev, .swiper-container-cube .swiper-slide-next + .swiper-slide { pointer-events: auto; visibility: visible; } .swiper-container-cube .swiper-slide-shadow-top, .swiper-container-cube .swiper-slide-shadow-bottom, .swiper-container-cube .swiper-slide-shadow-left, .swiper-container-cube .swiper-slide-shadow-right { z-index: 0; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden; } .swiper-container-cube .swiper-cube-shadow { position: absolute; left: 0; bottom: 0px; width: 100%; height: 100%; background: #000; opacity: 0.6; -webkit-filter: blur(50px); filter: blur(50px); z-index: 0; } /* Scrollbar */ .swiper-scrollbar { border-radius: 10px; position: relative; -ms-touch-action: none; background: rgba(0, 0, 0, 0.1); } .swiper-container-horizontal > .swiper-scrollbar { position: absolute; left: 1%; bottom: 3px; z-index: 50; height: 5px; width: 98%; } .swiper-container-vertical > .swiper-scrollbar { position: absolute; right: 3px; top: 1%; z-index: 50; width: 5px; height: 98%; } .swiper-scrollbar-drag { height: 100%; width: 100%; position: relative; background: rgba(0, 0, 0, 0.5); border-radius: 10px; left: 0; top: 0; } .swiper-scrollbar-cursor-drag { cursor: move; } /* Preloader */ .swiper-lazy-preloader { width: 42px; height: 42px; position: absolute; left: 50%; top: 50%; margin-left: -21px; margin-top: -21px; z-index: 10; -webkit-transform-origin: 50%; -moz-transform-origin: 50%; transform-origin: 50%; -webkit-animation: swiper-preloader-spin 1s steps(12, end) infinite; -moz-animation: swiper-preloader-spin 1s steps(12, end) infinite; animation: swiper-preloader-spin 1s steps(12, end) infinite; } .swiper-lazy-preloader:after { display: block; content: ""; width: 100%; height: 100%; background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20viewBox%3D'0%200%20120%20120'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20xmlns%3Axlink%3D'http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink'%3E%3Cdefs%3E%3Cline%20id%3D'l'%20x1%3D'60'%20x2%3D'60'%20y1%3D'7'%20y2%3D'27'%20stroke%3D'%236c6c6c'%20stroke-width%3D'11'%20stroke-linecap%3D'round'%2F%3E%3C%2Fdefs%3E%3Cg%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(30%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(60%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(90%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(120%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(150%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.37'%20transform%3D'rotate(180%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.46'%20transform%3D'rotate(210%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.56'%20transform%3D'rotate(240%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.66'%20transform%3D'rotate(270%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.75'%20transform%3D'rotate(300%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.85'%20transform%3D'rotate(330%2060%2C60)'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); background-position: 50%; -webkit-background-size: 100%; background-size: 100%; background-repeat: no-repeat; } .swiper-lazy-preloader-white:after { background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20viewBox%3D'0%200%20120%20120'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20xmlns%3Axlink%3D'http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink'%3E%3Cdefs%3E%3Cline%20id%3D'l'%20x1%3D'60'%20x2%3D'60'%20y1%3D'7'%20y2%3D'27'%20stroke%3D'%23fff'%20stroke-width%3D'11'%20stroke-linecap%3D'round'%2F%3E%3C%2Fdefs%3E%3Cg%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(30%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(60%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(90%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(120%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.27'%20transform%3D'rotate(150%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.37'%20transform%3D'rotate(180%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.46'%20transform%3D'rotate(210%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.56'%20transform%3D'rotate(240%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.66'%20transform%3D'rotate(270%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.75'%20transform%3D'rotate(300%2060%2C60)'%2F%3E%3Cuse%20xlink%3Ahref%3D'%23l'%20opacity%3D'.85'%20transform%3D'rotate(330%2060%2C60)'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); } @-webkit-keyframes swiper-preloader-spin { 100% { -webkit-transform: rotate(360deg); } } @keyframes swiper-preloader-spin { 100% { transform: rotate(360deg); } } ion-slides { width: 100%; height: 100%; display: block; } .slide-zoom { display: block; width: 100%; text-align: center; } .swiper-container { width: 100%; height: 100%; padding: 0; overflow: hidden; } .swiper-wrapper { position: absolute; left: 0; top: 0; width: 100%; height: 100%; padding: 0; } .swiper-slide { width: 100%; height: 100%; box-sizing: border-box; /* Center slide text vertically */ } .swiper-slide img { width: auto; height: auto; max-width: 100%; max-height: 100%; } .scroll-refresher { position: absolute; top: -60px; right: 0; left: 0; overflow: hidden; margin: auto; height: 60px; } .scroll-refresher .ionic-refresher-content { position: absolute; bottom: 15px; left: 0; width: 100%; color: #666666; text-align: center; font-size: 30px; } .scroll-refresher .ionic-refresher-content .text-refreshing, .scroll-refresher .ionic-refresher-content .text-pulling { font-size: 16px; line-height: 16px; } .scroll-refresher .ionic-refresher-content.ionic-refresher-with-text { bottom: 10px; } .scroll-refresher .icon-refreshing, .scroll-refresher .icon-pulling { width: 100%; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-transform-style: preserve-3d; transform-style: preserve-3d; } .scroll-refresher .icon-pulling { -webkit-animation-name: refresh-spin-back; animation-name: refresh-spin-back; -webkit-animation-duration: 200ms; animation-duration: 200ms; -webkit-animation-timing-function: linear; animation-timing-function: linear; -webkit-animation-fill-mode: none; animation-fill-mode: none; -webkit-transform: translate3d(0, 0, 0) rotate(0deg); transform: translate3d(0, 0, 0) rotate(0deg); } .scroll-refresher .icon-refreshing, .scroll-refresher .text-refreshing { display: none; } .scroll-refresher .icon-refreshing { -webkit-animation-duration: 1.5s; animation-duration: 1.5s; } .scroll-refresher.active .icon-pulling:not(.pulling-rotation-disabled) { -webkit-animation-name: refresh-spin; animation-name: refresh-spin; -webkit-transform: translate3d(0, 0, 0) rotate(-180deg); transform: translate3d(0, 0, 0) rotate(-180deg); } .scroll-refresher.active.refreshing { -webkit-transition: -webkit-transform 0.2s; transition: -webkit-transform 0.2s; -webkit-transition: transform 0.2s; transition: transform 0.2s; -webkit-transform: scale(1, 1); transform: scale(1, 1); } .scroll-refresher.active.refreshing .icon-pulling, .scroll-refresher.active.refreshing .text-pulling { display: none; } .scroll-refresher.active.refreshing .icon-refreshing, .scroll-refresher.active.refreshing .text-refreshing { display: block; } .scroll-refresher.active.refreshing.refreshing-tail { -webkit-transform: scale(0, 0); transform: scale(0, 0); } .overflow-scroll > .scroll { -webkit-overflow-scrolling: touch; width: 100%; } .overflow-scroll > .scroll.overscroll { position: fixed; right: 0; left: 0; } .overflow-scroll.padding > .scroll.overscroll { padding: 10px; } @-webkit-keyframes refresh-spin { 0% { -webkit-transform: translate3d(0, 0, 0) rotate(0); } 100% { -webkit-transform: translate3d(0, 0, 0) rotate(180deg); } } @keyframes refresh-spin { 0% { transform: translate3d(0, 0, 0) rotate(0); } 100% { transform: translate3d(0, 0, 0) rotate(180deg); } } @-webkit-keyframes refresh-spin-back { 0% { -webkit-transform: translate3d(0, 0, 0) rotate(180deg); } 100% { -webkit-transform: translate3d(0, 0, 0) rotate(0); } } @keyframes refresh-spin-back { 0% { transform: translate3d(0, 0, 0) rotate(180deg); } 100% { transform: translate3d(0, 0, 0) rotate(0); } } /** * Spinners * -------------------------------------------------- */ .spinner { stroke: #444; fill: #444; } .spinner svg { width: 28px; height: 28px; } .spinner.spinner-light { stroke: #fff; fill: #fff; } .spinner.spinner-stable { stroke: #f8f8f8; fill: #f8f8f8; } .spinner.spinner-positive { stroke: #387ef5; fill: #387ef5; } .spinner.spinner-calm { stroke: #11c1f3; fill: #11c1f3; } .spinner.spinner-balanced { stroke: #33cd5f; fill: #33cd5f; } .spinner.spinner-assertive { stroke: #ef473a; fill: #ef473a; } .spinner.spinner-energized { stroke: #ffc900; fill: #ffc900; } .spinner.spinner-royal { stroke: #886aea; fill: #886aea; } .spinner.spinner-dark { stroke: #444; fill: #444; } .spinner-android { stroke: #4b8bf4; } .spinner-ios, .spinner-ios-small { stroke: #69717d; } .spinner-spiral .stop1 { stop-color: #fff; stop-opacity: 0; } .spinner-spiral.spinner-light .stop1 { stop-color: #444; } .spinner-spiral.spinner-light .stop2 { stop-color: #fff; } .spinner-spiral.spinner-stable .stop2 { stop-color: #f8f8f8; } .spinner-spiral.spinner-positive .stop2 { stop-color: #387ef5; } .spinner-spiral.spinner-calm .stop2 { stop-color: #11c1f3; } .spinner-spiral.spinner-balanced .stop2 { stop-color: #33cd5f; } .spinner-spiral.spinner-assertive .stop2 { stop-color: #ef473a; } .spinner-spiral.spinner-energized .stop2 { stop-color: #ffc900; } .spinner-spiral.spinner-royal .stop2 { stop-color: #886aea; } .spinner-spiral.spinner-dark .stop2 { stop-color: #444; } /** * Forms * -------------------------------------------------- */ form { margin: 0 0 1.42857; } legend { display: block; margin-bottom: 1.42857; padding: 0; width: 100%; border: 1px solid #ddd; color: #444; font-size: 21px; line-height: 2.85714; } legend small { color: #f8f8f8; font-size: 1.07143; } label, input, button, select, textarea { font-weight: normal; font-size: 14px; line-height: 1.42857; } input, button, select, textarea { font-family: "-apple-system", "Helvetica Neue", "Roboto", "Segoe UI", sans-serif; } .item-input { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; position: relative; overflow: hidden; padding: 6px 0 5px 16px; } .item-input input { -webkit-border-radius: 0; border-radius: 0; -webkit-box-flex: 1; -webkit-flex: 1 220px; -moz-box-flex: 1; -moz-flex: 1 220px; -ms-flex: 1 220px; flex: 1 220px; -webkit-appearance: none; -moz-appearance: none; appearance: none; margin: 0; padding-right: 24px; background-color: transparent; } .item-input .button .icon { -webkit-box-flex: 0; -webkit-flex: 0 0 24px; -moz-box-flex: 0; -moz-flex: 0 0 24px; -ms-flex: 0 0 24px; flex: 0 0 24px; position: static; display: inline-block; height: auto; text-align: center; font-size: 16px; } .item-input .button-bar { -webkit-border-radius: 0; border-radius: 0; -webkit-box-flex: 1; -webkit-flex: 1 0 220px; -moz-box-flex: 1; -moz-flex: 1 0 220px; -ms-flex: 1 0 220px; flex: 1 0 220px; -webkit-appearance: none; -moz-appearance: none; appearance: none; } .item-input .icon { min-width: 14px; } .platform-windowsphone .item-input input { flex-shrink: 1; } .item-input-inset { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; position: relative; overflow: hidden; padding: 10.66667px; } .item-input-wrapper { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-flex: 1; -webkit-flex: 1 0; -moz-box-flex: 1; -moz-flex: 1 0; -ms-flex: 1 0; flex: 1 0; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; -webkit-border-radius: 4px; border-radius: 4px; padding-right: 8px; padding-left: 8px; background: #eee; } .item-input-inset .item-input-wrapper input { padding-left: 4px; height: 29px; background: transparent; line-height: 18px; } .item-input-wrapper ~ .button { margin-left: 10.66667px; } .input-label { display: table; padding: 7px 10px 7px 0px; max-width: 200px; width: 35%; color: #444; font-size: 16px; } .placeholder-icon { color: #aaa; } .placeholder-icon:first-child { padding-right: 6px; } .placeholder-icon:last-child { padding-left: 6px; } .item-stacked-label { display: block; background-color: transparent; box-shadow: none; } .item-stacked-label .input-label, .item-stacked-label .icon { display: inline-block; padding: 4px 0 0 0px; vertical-align: middle; } .item-stacked-label input, .item-stacked-label textarea { -webkit-border-radius: 2px; border-radius: 2px; padding: 4px 8px 3px 0; border: none; background-color: #fff; } .item-stacked-label input { overflow: hidden; height: 46px; } .item-select.item-stacked-label select { position: relative; padding: 0px; max-width: 90%; direction: ltr; white-space: pre-wrap; margin: -3px; } .item-floating-label { display: block; background-color: transparent; box-shadow: none; } .item-floating-label .input-label { position: relative; padding: 5px 0 0 0; opacity: 0; top: 10px; -webkit-transition: opacity 0.15s ease-in, top 0.2s linear; transition: opacity 0.15s ease-in, top 0.2s linear; } .item-floating-label .input-label.has-input { opacity: 1; top: 0; -webkit-transition: opacity 0.15s ease-in, top 0.2s linear; transition: opacity 0.15s ease-in, top 0.2s linear; } textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"] { display: block; padding-top: 2px; padding-left: 0; height: 34px; color: #111; vertical-align: middle; font-size: 14px; line-height: 16px; } .platform-ios input[type="datetime-local"], .platform-ios input[type="date"], .platform-ios input[type="month"], .platform-ios input[type="time"], .platform-ios input[type="week"], .platform-android input[type="datetime-local"], .platform-android input[type="date"], .platform-android input[type="month"], .platform-android input[type="time"], .platform-android input[type="week"] { padding-top: 8px; } .item-input input, .item-input textarea { width: 100%; } textarea { padding-left: 0; } textarea::-moz-placeholder { color: #aaaaaa; } textarea:-ms-input-placeholder { color: #aaaaaa; } textarea::-webkit-input-placeholder { color: #aaaaaa; text-indent: -3px; } textarea { height: auto; } textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"] { border: 0; } input[type="radio"], input[type="checkbox"] { margin: 0; line-height: normal; } .item-input input[type="file"], .item-input input[type="image"], .item-input input[type="submit"], .item-input input[type="reset"], .item-input input[type="button"], .item-input input[type="radio"], .item-input input[type="checkbox"] { width: auto; } input[type="file"] { line-height: 34px; } .previous-input-focus, .cloned-text-input + input, .cloned-text-input + textarea { position: absolute !important; left: -9999px; width: 200px; } input::-moz-placeholder, textarea::-moz-placeholder { color: #aaaaaa; } input:-ms-input-placeholder, textarea:-ms-input-placeholder { color: #aaaaaa; } input::-webkit-input-placeholder, textarea::-webkit-input-placeholder { color: #aaaaaa; text-indent: 0; } input[disabled], select[disabled], textarea[disabled], input[readonly]:not(.cloned-text-input), textarea[readonly]:not(.cloned-text-input), select[readonly] { background-color: #f8f8f8; cursor: not-allowed; } input[type="radio"][disabled], input[type="checkbox"][disabled], input[type="radio"][readonly], input[type="checkbox"][readonly] { background-color: transparent; } /** * Checkbox * -------------------------------------------------- */ .checkbox { position: relative; display: inline-block; padding: 7px 7px; cursor: pointer; } .checkbox input:before, .checkbox .checkbox-icon:before { border-color: #ddd; } .checkbox input:checked:before, .checkbox input:checked + .checkbox-icon:before { background: #387ef5; border-color: #387ef5; } .checkbox-light input:before, .checkbox-light .checkbox-icon:before { border-color: #ddd; } .checkbox-light input:checked:before, .checkbox-light input:checked + .checkbox-icon:before { background: #ddd; border-color: #ddd; } .checkbox-stable input:before, .checkbox-stable .checkbox-icon:before { border-color: #b2b2b2; } .checkbox-stable input:checked:before, .checkbox-stable input:checked + .checkbox-icon:before { background: #b2b2b2; border-color: #b2b2b2; } .checkbox-positive input:before, .checkbox-positive .checkbox-icon:before { border-color: #387ef5; } .checkbox-positive input:checked:before, .checkbox-positive input:checked + .checkbox-icon:before { background: #387ef5; border-color: #387ef5; } .checkbox-calm input:before, .checkbox-calm .checkbox-icon:before { border-color: #11c1f3; } .checkbox-calm input:checked:before, .checkbox-calm input:checked + .checkbox-icon:before { background: #11c1f3; border-color: #11c1f3; } .checkbox-assertive input:before, .checkbox-assertive .checkbox-icon:before { border-color: #ef473a; } .checkbox-assertive input:checked:before, .checkbox-assertive input:checked + .checkbox-icon:before { background: #ef473a; border-color: #ef473a; } .checkbox-balanced input:before, .checkbox-balanced .checkbox-icon:before { border-color: #33cd5f; } .checkbox-balanced input:checked:before, .checkbox-balanced input:checked + .checkbox-icon:before { background: #33cd5f; border-color: #33cd5f; } .checkbox-energized input:before, .checkbox-energized .checkbox-icon:before { border-color: #ffc900; } .checkbox-energized input:checked:before, .checkbox-energized input:checked + .checkbox-icon:before { background: #ffc900; border-color: #ffc900; } .checkbox-royal input:before, .checkbox-royal .checkbox-icon:before { border-color: #886aea; } .checkbox-royal input:checked:before, .checkbox-royal input:checked + .checkbox-icon:before { background: #886aea; border-color: #886aea; } .checkbox-dark input:before, .checkbox-dark .checkbox-icon:before { border-color: #444; } .checkbox-dark input:checked:before, .checkbox-dark input:checked + .checkbox-icon:before { background: #444; border-color: #444; } .checkbox input:disabled:before, .checkbox input:disabled + .checkbox-icon:before { border-color: #ddd; } .checkbox input:disabled:checked:before, .checkbox input:disabled:checked + .checkbox-icon:before { background: #ddd; } .checkbox.checkbox-input-hidden input { display: none !important; } .checkbox input, .checkbox-icon { position: relative; width: 28px; height: 28px; display: block; border: 0; background: transparent; cursor: pointer; -webkit-appearance: none; } .checkbox input:before, .checkbox-icon:before { display: table; width: 100%; height: 100%; border-width: 1px; border-style: solid; border-radius: 28px; background: #fff; content: ' '; -webkit-transition: background-color 20ms ease-in-out; transition: background-color 20ms ease-in-out; } .checkbox input:checked:before, input:checked + .checkbox-icon:before { border-width: 2px; } .checkbox input:after, .checkbox-icon:after { -webkit-transition: opacity 0.05s ease-in-out; transition: opacity 0.05s ease-in-out; -webkit-transform: rotate(-45deg); transform: rotate(-45deg); position: absolute; top: 33%; left: 25%; display: table; width: 14px; height: 6px; border: 1px solid #fff; border-top: 0; border-right: 0; content: ' '; opacity: 0; } .platform-android .checkbox-platform input:before, .platform-android .checkbox-platform .checkbox-icon:before, .checkbox-square input:before, .checkbox-square .checkbox-icon:before { border-radius: 2px; width: 72%; height: 72%; margin-top: 14%; margin-left: 14%; border-width: 2px; } .platform-android .checkbox-platform input:after, .platform-android .checkbox-platform .checkbox-icon:after, .checkbox-square input:after, .checkbox-square .checkbox-icon:after { border-width: 2px; top: 19%; left: 25%; width: 13px; height: 7px; } .platform-android .item-checkbox-right .checkbox-square .checkbox-icon::after { top: 31%; } .grade-c .checkbox input:after, .grade-c .checkbox-icon:after { -webkit-transform: rotate(0); transform: rotate(0); top: 3px; left: 4px; border: none; color: #fff; content: '\2713'; font-weight: bold; font-size: 20px; } .checkbox input:checked:after, input:checked + .checkbox-icon:after { opacity: 1; } .item-checkbox { padding-left: 60px; } .item-checkbox.active { box-shadow: none; } .item-checkbox .checkbox { position: absolute; top: 50%; right: 8px; left: 8px; z-index: 3; margin-top: -21px; } .item-checkbox.item-checkbox-right { padding-right: 60px; padding-left: 16px; } .item-checkbox-right .checkbox input, .item-checkbox-right .checkbox-icon { float: right; } /** * Toggle * -------------------------------------------------- */ .item-toggle { pointer-events: none; } .toggle { position: relative; display: inline-block; pointer-events: auto; margin: -5px; padding: 5px; } .toggle input:checked + .track { border-color: #4cd964; background-color: #4cd964; } .toggle.dragging .handle { background-color: #f2f2f2 !important; } .toggle.toggle-light input:checked + .track { border-color: #ddd; background-color: #ddd; } .toggle.toggle-stable input:checked + .track { border-color: #b2b2b2; background-color: #b2b2b2; } .toggle.toggle-positive input:checked + .track { border-color: #387ef5; background-color: #387ef5; } .toggle.toggle-calm input:checked + .track { border-color: #11c1f3; background-color: #11c1f3; } .toggle.toggle-assertive input:checked + .track { border-color: #ef473a; background-color: #ef473a; } .toggle.toggle-balanced input:checked + .track { border-color: #33cd5f; background-color: #33cd5f; } .toggle.toggle-energized input:checked + .track { border-color: #ffc900; background-color: #ffc900; } .toggle.toggle-royal input:checked + .track { border-color: #886aea; background-color: #886aea; } .toggle.toggle-dark input:checked + .track { border-color: #444; background-color: #444; } .toggle input { display: none; } /* the track appearance when the toggle is "off" */ .toggle .track { -webkit-transition-timing-function: ease-in-out; transition-timing-function: ease-in-out; -webkit-transition-duration: 0.3s; transition-duration: 0.3s; -webkit-transition-property: background-color, border; transition-property: background-color, border; display: inline-block; box-sizing: border-box; width: 51px; height: 31px; border: solid 2px #e6e6e6; border-radius: 20px; background-color: #fff; content: ' '; cursor: pointer; pointer-events: none; } /* Fix to avoid background color bleeding */ /* (occured on (at least) Android 4.2, Asus MeMO Pad HD7 ME173X) */ .platform-android4_2 .toggle .track { -webkit-background-clip: padding-box; } /* the handle (circle) thats inside the toggle's track area */ /* also the handle's appearance when it is "off" */ .toggle .handle { -webkit-transition: 0.3s cubic-bezier(0, 1.1, 1, 1.1); transition: 0.3s cubic-bezier(0, 1.1, 1, 1.1); -webkit-transition-property: background-color, transform; transition-property: background-color, transform; position: absolute; display: block; width: 27px; height: 27px; border-radius: 27px; background-color: #fff; top: 7px; left: 7px; box-shadow: 0 2px 7px rgba(0, 0, 0, 0.35), 0 1px 1px rgba(0, 0, 0, 0.15); } .toggle .handle:before { position: absolute; top: -4px; left: -21.5px; padding: 18.5px 34px; content: " "; } .toggle input:checked + .track .handle { -webkit-transform: translate3d(20px, 0, 0); transform: translate3d(20px, 0, 0); background-color: #fff; } .item-toggle.active { box-shadow: none; } .item-toggle, .item-toggle.item-complex .item-content { padding-right: 99px; } .item-toggle.item-complex { padding-right: 0; } .item-toggle .toggle { position: absolute; top: 10px; right: 16px; z-index: 3; } .toggle input:disabled + .track { opacity: .6; } .toggle-small .track { border: 0; width: 34px; height: 15px; background: #9e9e9e; } .toggle-small input:checked + .track { background: rgba(0, 150, 137, 0.5); } .toggle-small .handle { top: 2px; left: 4px; width: 21px; height: 21px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25); } .toggle-small input:checked + .track .handle { -webkit-transform: translate3d(16px, 0, 0); transform: translate3d(16px, 0, 0); background: #009689; } .toggle-small.item-toggle .toggle { top: 19px; } .toggle-small .toggle-light input:checked + .track { background-color: rgba(221, 221, 221, 0.5); } .toggle-small .toggle-light input:checked + .track .handle { background-color: #ddd; } .toggle-small .toggle-stable input:checked + .track { background-color: rgba(178, 178, 178, 0.5); } .toggle-small .toggle-stable input:checked + .track .handle { background-color: #b2b2b2; } .toggle-small .toggle-positive input:checked + .track { background-color: rgba(56, 126, 245, 0.5); } .toggle-small .toggle-positive input:checked + .track .handle { background-color: #387ef5; } .toggle-small .toggle-calm input:checked + .track { background-color: rgba(17, 193, 243, 0.5); } .toggle-small .toggle-calm input:checked + .track .handle { background-color: #11c1f3; } .toggle-small .toggle-assertive input:checked + .track { background-color: rgba(239, 71, 58, 0.5); } .toggle-small .toggle-assertive input:checked + .track .handle { background-color: #ef473a; } .toggle-small .toggle-balanced input:checked + .track { background-color: rgba(51, 205, 95, 0.5); } .toggle-small .toggle-balanced input:checked + .track .handle { background-color: #33cd5f; } .toggle-small .toggle-energized input:checked + .track { background-color: rgba(255, 201, 0, 0.5); } .toggle-small .toggle-energized input:checked + .track .handle { background-color: #ffc900; } .toggle-small .toggle-royal input:checked + .track { background-color: rgba(136, 106, 234, 0.5); } .toggle-small .toggle-royal input:checked + .track .handle { background-color: #886aea; } .toggle-small .toggle-dark input:checked + .track { background-color: rgba(68, 68, 68, 0.5); } .toggle-small .toggle-dark input:checked + .track .handle { background-color: #444; } /** * Radio Button Inputs * -------------------------------------------------- */ .item-radio { padding: 0; } .item-radio:hover { cursor: pointer; } .item-radio .item-content { /* give some room to the right for the checkmark icon */ padding-right: 64px; } .item-radio .radio-icon { /* checkmark icon will be hidden by default */ position: absolute; top: 0; right: 0; z-index: 3; visibility: hidden; padding: 14px; height: 100%; font-size: 24px; } .item-radio input { /* hide any radio button inputs elements (the ugly circles) */ position: absolute; left: -9999px; } .item-radio input:checked + .radio-content .item-content { /* style the item content when its checked */ background: #f7f7f7; } .item-radio input:checked + .radio-content .radio-icon { /* show the checkmark icon when its checked */ visibility: visible; } /** * Range * -------------------------------------------------- */ .range input { display: inline-block; overflow: hidden; margin-top: 5px; margin-bottom: 5px; padding-right: 2px; padding-left: 1px; width: auto; height: 43px; outline: none; background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ccc), color-stop(100%, #ccc)); background: linear-gradient(to right, #ccc 0%, #ccc 100%); background-position: center; background-size: 99% 2px; background-repeat: no-repeat; -webkit-appearance: none; /* &::-ms-track{ background: transparent; border-color: transparent; border-width: 11px 0 16px; color:transparent; margin-top:20px; } &::-ms-thumb { width: $range-slider-width; height: $range-slider-height; border-radius: $range-slider-border-radius; background-color: $toggle-handle-off-bg-color; border-color:$toggle-handle-off-bg-color; box-shadow: $range-slider-box-shadow; margin-left:1px; margin-right:1px; outline:none; } &::-ms-fill-upper { height: $range-track-height; background:$range-default-track-bg; } */ } .range input::-moz-focus-outer { /* hide the focus outline in Firefox */ border: 0; } .range input::-webkit-slider-thumb { position: relative; width: 28px; height: 28px; border-radius: 50%; background-color: #fff; box-shadow: 0 0 2px rgba(0, 0, 0, 0.3), 0 3px 5px rgba(0, 0, 0, 0.2); cursor: pointer; -webkit-appearance: none; border: 0; } .range input::-webkit-slider-thumb:before { /* what creates the colorful line on the left side of the slider */ position: absolute; top: 13px; left: -2001px; width: 2000px; height: 2px; background: #444; content: ' '; } .range input::-webkit-slider-thumb:after { /* create a larger (but hidden) hit area */ position: absolute; top: -15px; left: -15px; padding: 30px; content: ' '; } .range input::-ms-fill-lower { height: 2px; background: #444; } .range { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; padding: 2px 11px; } .range.range-light input::-webkit-slider-thumb:before { background: #ddd; } .range.range-light input::-ms-fill-lower { background: #ddd; } .range.range-stable input::-webkit-slider-thumb:before { background: #b2b2b2; } .range.range-stable input::-ms-fill-lower { background: #b2b2b2; } .range.range-positive input::-webkit-slider-thumb:before { background: #387ef5; } .range.range-positive input::-ms-fill-lower { background: #387ef5; } .range.range-calm input::-webkit-slider-thumb:before { background: #11c1f3; } .range.range-calm input::-ms-fill-lower { background: #11c1f3; } .range.range-balanced input::-webkit-slider-thumb:before { background: #33cd5f; } .range.range-balanced input::-ms-fill-lower { background: #33cd5f; } .range.range-assertive input::-webkit-slider-thumb:before { background: #ef473a; } .range.range-assertive input::-ms-fill-lower { background: #ef473a; } .range.range-energized input::-webkit-slider-thumb:before { background: #ffc900; } .range.range-energized input::-ms-fill-lower { background: #ffc900; } .range.range-royal input::-webkit-slider-thumb:before { background: #886aea; } .range.range-royal input::-ms-fill-lower { background: #886aea; } .range.range-dark input::-webkit-slider-thumb:before { background: #444; } .range.range-dark input::-ms-fill-lower { background: #444; } .range .icon { -webkit-box-flex: 0; -webkit-flex: 0; -moz-box-flex: 0; -moz-flex: 0; -ms-flex: 0; flex: 0; display: block; min-width: 24px; text-align: center; font-size: 24px; } .range input { -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; display: block; margin-right: 10px; margin-left: 10px; } .range-label { -webkit-box-flex: 0; -webkit-flex: 0 0 auto; -moz-box-flex: 0; -moz-flex: 0 0 auto; -ms-flex: 0 0 auto; flex: 0 0 auto; display: block; white-space: nowrap; } .range-label:first-child { padding-left: 5px; } .range input + .range-label { padding-right: 5px; padding-left: 0; } .platform-windowsphone .range input { height: auto; } /** * Select * -------------------------------------------------- */ .item-select { position: relative; } .item-select select { -webkit-appearance: none; -moz-appearance: none; appearance: none; position: absolute; top: 0; bottom: 0; right: 0; padding: 0 48px 0 16px; max-width: 65%; border: none; background: #fff; color: #333; text-indent: .01px; text-overflow: ''; white-space: nowrap; font-size: 14px; cursor: pointer; direction: rtl; } .item-select select::-ms-expand { display: none; } .item-select option { direction: ltr; } .item-select:after { position: absolute; top: 50%; right: 16px; margin-top: -3px; width: 0; height: 0; border-top: 5px solid; border-right: 5px solid transparent; border-left: 5px solid transparent; color: #999; content: ""; pointer-events: none; } .item-select.item-light select { background: #fff; color: #444; } .item-select.item-stable select { background: #f8f8f8; color: #444; } .item-select.item-stable:after, .item-select.item-stable .input-label { color: #666666; } .item-select.item-positive select { background: #387ef5; color: #fff; } .item-select.item-positive:after, .item-select.item-positive .input-label { color: #fff; } .item-select.item-calm select { background: #11c1f3; color: #fff; } .item-select.item-calm:after, .item-select.item-calm .input-label { color: #fff; } .item-select.item-assertive select { background: #ef473a; color: #fff; } .item-select.item-assertive:after, .item-select.item-assertive .input-label { color: #fff; } .item-select.item-balanced select { background: #33cd5f; color: #fff; } .item-select.item-balanced:after, .item-select.item-balanced .input-label { color: #fff; } .item-select.item-energized select { background: #ffc900; color: #fff; } .item-select.item-energized:after, .item-select.item-energized .input-label { color: #fff; } .item-select.item-royal select { background: #886aea; color: #fff; } .item-select.item-royal:after, .item-select.item-royal .input-label { color: #fff; } .item-select.item-dark select { background: #444; color: #fff; } .item-select.item-dark:after, .item-select.item-dark .input-label { color: #fff; } select[multiple], select[size] { height: auto; } /** * Progress * -------------------------------------------------- */ progress { display: block; margin: 15px auto; width: 100%; } /** * Buttons * -------------------------------------------------- */ .button { border-color: transparent; background-color: #f8f8f8; color: #444; position: relative; display: inline-block; margin: 0; padding: 0 12px; min-width: 52px; min-height: 47px; border-width: 1px; border-style: solid; border-radius: 4px; vertical-align: top; text-align: center; text-overflow: ellipsis; font-size: 16px; line-height: 42px; cursor: pointer; } .button:hover { color: #444; text-decoration: none; } .button.active, .button.activated { background-color: #e5e5e5; } .button:after { position: absolute; top: -6px; right: -6px; bottom: -6px; left: -6px; content: ' '; } .button .icon { vertical-align: top; pointer-events: none; } .button .icon:before, .button.icon:before, .button.icon-left:before, .button.icon-right:before { display: inline-block; padding: 0 0 1px 0; vertical-align: inherit; font-size: 24px; line-height: 41px; pointer-events: none; } .button.icon-left:before { float: left; padding-right: .2em; padding-left: 0; } .button.icon-right:before { float: right; padding-right: 0; padding-left: .2em; } .button.button-block, .button.button-full { margin-top: 10px; margin-bottom: 10px; } .button.button-light { border-color: transparent; background-color: #fff; color: #444; } .button.button-light:hover { color: #444; text-decoration: none; } .button.button-light.active, .button.button-light.activated { background-color: #fafafa; } .button.button-light.button-clear { border-color: transparent; background: none; box-shadow: none; color: #ddd; } .button.button-light.button-icon { border-color: transparent; background: none; } .button.button-light.button-outline { border-color: #ddd; background: transparent; color: #ddd; } .button.button-light.button-outline.active, .button.button-light.button-outline.activated { background-color: #ddd; box-shadow: none; color: #fff; } .button.button-stable { border-color: transparent; background-color: #f8f8f8; color: #444; } .button.button-stable:hover { color: #444; text-decoration: none; } .button.button-stable.active, .button.button-stable.activated { background-color: #e5e5e5; } .button.button-stable.button-clear { border-color: transparent; background: none; box-shadow: none; color: #b2b2b2; } .button.button-stable.button-icon { border-color: transparent; background: none; } .button.button-stable.button-outline { border-color: #b2b2b2; background: transparent; color: #b2b2b2; } .button.button-stable.button-outline.active, .button.button-stable.button-outline.activated { background-color: #b2b2b2; box-shadow: none; color: #fff; } .button.button-positive { border-color: transparent; background-color: #387ef5; color: #fff; } .button.button-positive:hover { color: #fff; text-decoration: none; } .button.button-positive.active, .button.button-positive.activated { background-color: #0c60ee; } .button.button-positive.button-clear { border-color: transparent; background: none; box-shadow: none; color: #387ef5; } .button.button-positive.button-icon { border-color: transparent; background: none; } .button.button-positive.button-outline { border-color: #387ef5; background: transparent; color: #387ef5; } .button.button-positive.button-outline.active, .button.button-positive.button-outline.activated { background-color: #387ef5; box-shadow: none; color: #fff; } .button.button-calm { border-color: transparent; background-color: #11c1f3; color: #fff; } .button.button-calm:hover { color: #fff; text-decoration: none; } .button.button-calm.active, .button.button-calm.activated { background-color: #0a9dc7; } .button.button-calm.button-clear { border-color: transparent; background: none; box-shadow: none; color: #11c1f3; } .button.button-calm.button-icon { border-color: transparent; background: none; } .button.button-calm.button-outline { border-color: #11c1f3; background: transparent; color: #11c1f3; } .button.button-calm.button-outline.active, .button.button-calm.button-outline.activated { background-color: #11c1f3; box-shadow: none; color: #fff; } .button.button-assertive { border-color: transparent; background-color: #ef473a; color: #fff; } .button.button-assertive:hover { color: #fff; text-decoration: none; } .button.button-assertive.active, .button.button-assertive.activated { background-color: #e42112; } .button.button-assertive.button-clear { border-color: transparent; background: none; box-shadow: none; color: #ef473a; } .button.button-assertive.button-icon { border-color: transparent; background: none; } .button.button-assertive.button-outline { border-color: #ef473a; background: transparent; color: #ef473a; } .button.button-assertive.button-outline.active, .button.button-assertive.button-outline.activated { background-color: #ef473a; box-shadow: none; color: #fff; } .button.button-balanced { border-color: transparent; background-color: #33cd5f; color: #fff; } .button.button-balanced:hover { color: #fff; text-decoration: none; } .button.button-balanced.active, .button.button-balanced.activated { background-color: #28a54c; } .button.button-balanced.button-clear { border-color: transparent; background: none; box-shadow: none; color: #33cd5f; } .button.button-balanced.button-icon { border-color: transparent; background: none; } .button.button-balanced.button-outline { border-color: #33cd5f; background: transparent; color: #33cd5f; } .button.button-balanced.button-outline.active, .button.button-balanced.button-outline.activated { background-color: #33cd5f; box-shadow: none; color: #fff; } .button.button-energized { border-color: transparent; background-color: #ffc900; color: #fff; } .button.button-energized:hover { color: #fff; text-decoration: none; } .button.button-energized.active, .button.button-energized.activated { background-color: #e6b500; } .button.button-energized.button-clear { border-color: transparent; background: none; box-shadow: none; color: #ffc900; } .button.button-energized.button-icon { border-color: transparent; background: none; } .button.button-energized.button-outline { border-color: #ffc900; background: transparent; color: #ffc900; } .button.button-energized.button-outline.active, .button.button-energized.button-outline.activated { background-color: #ffc900; box-shadow: none; color: #fff; } .button.button-royal { border-color: transparent; background-color: #886aea; color: #fff; } .button.button-royal:hover { color: #fff; text-decoration: none; } .button.button-royal.active, .button.button-royal.activated { background-color: #6b46e5; } .button.button-royal.button-clear { border-color: transparent; background: none; box-shadow: none; color: #886aea; } .button.button-royal.button-icon { border-color: transparent; background: none; } .button.button-royal.button-outline { border-color: #886aea; background: transparent; color: #886aea; } .button.button-royal.button-outline.active, .button.button-royal.button-outline.activated { background-color: #886aea; box-shadow: none; color: #fff; } .button.button-dark { border-color: transparent; background-color: #444; color: #fff; } .button.button-dark:hover { color: #fff; text-decoration: none; } .button.button-dark.active, .button.button-dark.activated { background-color: #262626; } .button.button-dark.button-clear { border-color: transparent; background: none; box-shadow: none; color: #444; } .button.button-dark.button-icon { border-color: transparent; background: none; } .button.button-dark.button-outline { border-color: #444; background: transparent; color: #444; } .button.button-dark.button-outline.active, .button.button-dark.button-outline.activated { background-color: #444; box-shadow: none; color: #fff; } .button-small { padding: 2px 4px 1px; min-width: 28px; min-height: 30px; font-size: 12px; line-height: 26px; } .button-small .icon:before, .button-small.icon:before, .button-small.icon-left:before, .button-small.icon-right:before { font-size: 16px; line-height: 19px; margin-top: 3px; } .button-large { padding: 0 16px; min-width: 68px; min-height: 59px; font-size: 20px; line-height: 53px; } .button-large .icon:before, .button-large.icon:before, .button-large.icon-left:before, .button-large.icon-right:before { padding-bottom: 2px; font-size: 32px; line-height: 51px; } .button-icon { -webkit-transition: opacity 0.1s; transition: opacity 0.1s; padding: 0 6px; min-width: initial; border-color: transparent; background: none; } .button-icon.button.active, .button-icon.button.activated { border-color: transparent; background: none; box-shadow: none; opacity: 0.3; } .button-icon .icon:before, .button-icon.icon:before { font-size: 32px; } .button-clear { -webkit-transition: opacity 0.1s; transition: opacity 0.1s; padding: 0 6px; max-height: 42px; border-color: transparent; background: none; box-shadow: none; } .button-clear.button-clear { border-color: transparent; background: none; box-shadow: none; color: #b2b2b2; } .button-clear.button-icon { border-color: transparent; background: none; } .button-clear.active, .button-clear.activated { opacity: 0.3; } .button-outline { -webkit-transition: opacity 0.1s; transition: opacity 0.1s; background: none; box-shadow: none; } .button-outline.button-outline { border-color: #b2b2b2; background: transparent; color: #b2b2b2; } .button-outline.button-outline.active, .button-outline.button-outline.activated { background-color: #b2b2b2; box-shadow: none; color: #fff; } .padding > .button.button-block:first-child { margin-top: 0; } .button-block { display: block; clear: both; } .button-block:after { clear: both; } .button-full, .button-full > .button { display: block; margin-right: 0; margin-left: 0; border-right-width: 0; border-left-width: 0; border-radius: 0; } button.button-block, button.button-full, .button-full > button.button, input.button.button-block { width: 100%; } a.button { text-decoration: none; } a.button .icon:before, a.button.icon:before, a.button.icon-left:before, a.button.icon-right:before { margin-top: 2px; } .button.disabled, .button[disabled] { opacity: .4; cursor: default !important; pointer-events: none; } /** * Button Bar * -------------------------------------------------- */ .button-bar { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; width: 100%; } .button-bar.button-bar-inline { display: block; width: auto; *zoom: 1; } .button-bar.button-bar-inline:before, .button-bar.button-bar-inline:after { display: table; content: ""; line-height: 0; } .button-bar.button-bar-inline:after { clear: both; } .button-bar.button-bar-inline > .button { width: auto; display: inline-block; float: left; } .button-bar > .button { -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; display: block; overflow: hidden; padding: 0 16px; width: 0; border-width: 1px 0px 1px 1px; border-radius: 0; text-align: center; text-overflow: ellipsis; white-space: nowrap; } .button-bar > .button:before, .button-bar > .button .icon:before { line-height: 44px; } .button-bar > .button:first-child { border-radius: 4px 0px 0px 4px; } .button-bar > .button:last-child { border-right-width: 1px; border-radius: 0px 4px 4px 0px; } .button-bar > .button:only-child { border-radius: 4px; } .button-bar > .button-small:before, .button-bar > .button-small .icon:before { line-height: 28px; } /** * Grid * -------------------------------------------------- * Using flexbox for the grid, inspired by Philip Walton: * http://philipwalton.github.io/solved-by-flexbox/demos/grids/ * By default each .col within a .row will evenly take up * available width, and the height of each .col with take * up the height of the tallest .col in the same .row. */ .row { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -moz-flex; display: -ms-flexbox; display: flex; padding: 5px; width: 100%; } .row-wrap { -webkit-flex-wrap: wrap; -moz-flex-wrap: wrap; -ms-flex-wrap: wrap; flex-wrap: wrap; } .row-no-padding { padding: 0; } .row-no-padding > .col { padding: 0; } .row + .row { margin-top: -5px; padding-top: 0; } .col { -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; display: block; padding: 5px; width: 100%; } /* Vertically Align Columns */ /* .row-* vertically aligns every .col in the .row */ .row-top { -webkit-box-align: start; -ms-flex-align: start; -webkit-align-items: flex-start; -moz-align-items: flex-start; align-items: flex-start; } .row-bottom { -webkit-box-align: end; -ms-flex-align: end; -webkit-align-items: flex-end; -moz-align-items: flex-end; align-items: flex-end; } .row-center { -webkit-box-align: center; -ms-flex-align: center; -webkit-align-items: center; -moz-align-items: center; align-items: center; } .row-stretch { -webkit-box-align: stretch; -ms-flex-align: stretch; -webkit-align-items: stretch; -moz-align-items: stretch; align-items: stretch; } .row-baseline { -webkit-box-align: baseline; -ms-flex-align: baseline; -webkit-align-items: baseline; -moz-align-items: baseline; align-items: baseline; } /* .col-* vertically aligns an individual .col */ .col-top { -webkit-align-self: flex-start; -moz-align-self: flex-start; -ms-flex-item-align: start; align-self: flex-start; } .col-bottom { -webkit-align-self: flex-end; -moz-align-self: flex-end; -ms-flex-item-align: end; align-self: flex-end; } .col-center { -webkit-align-self: center; -moz-align-self: center; -ms-flex-item-align: center; align-self: center; } /* Column Offsets */ .col-offset-10 { margin-left: 10%; } .col-offset-20 { margin-left: 20%; } .col-offset-25 { margin-left: 25%; } .col-offset-33, .col-offset-34 { margin-left: 33.3333%; } .col-offset-50 { margin-left: 50%; } .col-offset-66, .col-offset-67 { margin-left: 66.6666%; } .col-offset-75 { margin-left: 75%; } .col-offset-80 { margin-left: 80%; } .col-offset-90 { margin-left: 90%; } /* Explicit Column Percent Sizes */ /* By default each grid column will evenly distribute */ /* across the grid. However, you can specify individual */ /* columns to take up a certain size of the available area */ .col-10 { -webkit-box-flex: 0; -webkit-flex: 0 0 10%; -moz-box-flex: 0; -moz-flex: 0 0 10%; -ms-flex: 0 0 10%; flex: 0 0 10%; max-width: 10%; } .col-20 { -webkit-box-flex: 0; -webkit-flex: 0 0 20%; -moz-box-flex: 0; -moz-flex: 0 0 20%; -ms-flex: 0 0 20%; flex: 0 0 20%; max-width: 20%; } .col-25 { -webkit-box-flex: 0; -webkit-flex: 0 0 25%; -moz-box-flex: 0; -moz-flex: 0 0 25%; -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-33, .col-34 { -webkit-box-flex: 0; -webkit-flex: 0 0 33.3333%; -moz-box-flex: 0; -moz-flex: 0 0 33.3333%; -ms-flex: 0 0 33.3333%; flex: 0 0 33.3333%; max-width: 33.3333%; } .col-40 { -webkit-box-flex: 0; -webkit-flex: 0 0 40%; -moz-box-flex: 0; -moz-flex: 0 0 40%; -ms-flex: 0 0 40%; flex: 0 0 40%; max-width: 40%; } .col-50 { -webkit-box-flex: 0; -webkit-flex: 0 0 50%; -moz-box-flex: 0; -moz-flex: 0 0 50%; -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-60 { -webkit-box-flex: 0; -webkit-flex: 0 0 60%; -moz-box-flex: 0; -moz-flex: 0 0 60%; -ms-flex: 0 0 60%; flex: 0 0 60%; max-width: 60%; } .col-66, .col-67 { -webkit-box-flex: 0; -webkit-flex: 0 0 66.6666%; -moz-box-flex: 0; -moz-flex: 0 0 66.6666%; -ms-flex: 0 0 66.6666%; flex: 0 0 66.6666%; max-width: 66.6666%; } .col-75 { -webkit-box-flex: 0; -webkit-flex: 0 0 75%; -moz-box-flex: 0; -moz-flex: 0 0 75%; -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-80 { -webkit-box-flex: 0; -webkit-flex: 0 0 80%; -moz-box-flex: 0; -moz-flex: 0 0 80%; -ms-flex: 0 0 80%; flex: 0 0 80%; max-width: 80%; } .col-90 { -webkit-box-flex: 0; -webkit-flex: 0 0 90%; -moz-box-flex: 0; -moz-flex: 0 0 90%; -ms-flex: 0 0 90%; flex: 0 0 90%; max-width: 90%; } /* Responsive Grid Classes */ /* Adding a class of responsive-X to a row */ /* will trigger the flex-direction to */ /* change to column and add some margin */ /* to any columns in the row for clearity */ @media (max-width: 567px) { .responsive-sm { -webkit-box-direction: normal; -moz-box-direction: normal; -webkit-box-orient: vertical; -moz-box-orient: vertical; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; } .responsive-sm .col, .responsive-sm .col-10, .responsive-sm .col-20, .responsive-sm .col-25, .responsive-sm .col-33, .responsive-sm .col-34, .responsive-sm .col-50, .responsive-sm .col-66, .responsive-sm .col-67, .responsive-sm .col-75, .responsive-sm .col-80, .responsive-sm .col-90 { -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; margin-bottom: 15px; margin-left: 0; max-width: 100%; width: 100%; } } @media (max-width: 767px) { .responsive-md { -webkit-box-direction: normal; -moz-box-direction: normal; -webkit-box-orient: vertical; -moz-box-orient: vertical; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; } .responsive-md .col, .responsive-md .col-10, .responsive-md .col-20, .responsive-md .col-25, .responsive-md .col-33, .responsive-md .col-34, .responsive-md .col-50, .responsive-md .col-66, .responsive-md .col-67, .responsive-md .col-75, .responsive-md .col-80, .responsive-md .col-90 { -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; margin-bottom: 15px; margin-left: 0; max-width: 100%; width: 100%; } } @media (max-width: 1023px) { .responsive-lg { -webkit-box-direction: normal; -moz-box-direction: normal; -webkit-box-orient: vertical; -moz-box-orient: vertical; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; } .responsive-lg .col, .responsive-lg .col-10, .responsive-lg .col-20, .responsive-lg .col-25, .responsive-lg .col-33, .responsive-lg .col-34, .responsive-lg .col-50, .responsive-lg .col-66, .responsive-lg .col-67, .responsive-lg .col-75, .responsive-lg .col-80, .responsive-lg .col-90 { -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; margin-bottom: 15px; margin-left: 0; max-width: 100%; width: 100%; } } /** * Utility Classes * -------------------------------------------------- */ .hide { display: none; } .opacity-hide { opacity: 0; } .grade-b .opacity-hide, .grade-c .opacity-hide { opacity: 1; display: none; } .show { display: block; } .opacity-show { opacity: 1; } .invisible { visibility: hidden; } .keyboard-open .hide-on-keyboard-open { display: none; } .keyboard-open .tabs.hide-on-keyboard-open + .pane .has-tabs, .keyboard-open .bar-footer.hide-on-keyboard-open + .pane .has-footer { bottom: 0; } .inline { display: inline-block; } .disable-pointer-events { pointer-events: none; } .enable-pointer-events { pointer-events: auto; } .disable-user-behavior { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent; -webkit-user-drag: none; -ms-touch-action: none; -ms-content-zooming: none; } .click-block { position: absolute; top: 0; right: 0; bottom: 0; left: 0; opacity: 0; z-index: 99999; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); overflow: hidden; } .click-block-hide { -webkit-transform: translate3d(-9999px, 0, 0); transform: translate3d(-9999px, 0, 0); } .no-resize { resize: none; } .block { display: block; clear: both; } .block:after { display: block; visibility: hidden; clear: both; height: 0; content: "."; } .full-image { width: 100%; } .clearfix { *zoom: 1; } .clearfix:before, .clearfix:after { display: table; content: ""; line-height: 0; } .clearfix:after { clear: both; } /** * Content Padding * -------------------------------------------------- */ .padding { padding: 10px; } .padding-top, .padding-vertical { padding-top: 10px; } .padding-right, .padding-horizontal { padding-right: 10px; } .padding-bottom, .padding-vertical { padding-bottom: 10px; } .padding-left, .padding-horizontal { padding-left: 10px; } /** * Scrollable iFrames * -------------------------------------------------- */ .iframe-wrapper { position: fixed; -webkit-overflow-scrolling: touch; overflow: scroll; } .iframe-wrapper iframe { height: 100%; width: 100%; } /** * Rounded * -------------------------------------------------- */ .rounded { border-radius: 4px; } /** * Utility Colors * -------------------------------------------------- * Utility colors are added to help set a naming convention. You'll * notice we purposely do not use words like "red" or "blue", but * instead have colors which represent an emotion or generic theme. */ .light, a.light { color: #fff; } .light-bg { background-color: #fff; } .light-border { border-color: #ddd; } .stable, a.stable { color: #f8f8f8; } .stable-bg { background-color: #f8f8f8; } .stable-border { border-color: #b2b2b2; } .positive, a.positive { color: #387ef5; } .positive-bg { background-color: #387ef5; } .positive-border { border-color: #0c60ee; } .calm, a.calm { color: #11c1f3; } .calm-bg { background-color: #11c1f3; } .calm-border { border-color: #0a9dc7; } .assertive, a.assertive { color: #ef473a; } .assertive-bg { background-color: #ef473a; } .assertive-border { border-color: #e42112; } .balanced, a.balanced { color: #33cd5f; } .balanced-bg { background-color: #33cd5f; } .balanced-border { border-color: #28a54c; } .energized, a.energized { color: #ffc900; } .energized-bg { background-color: #ffc900; } .energized-border { border-color: #e6b500; } .royal, a.royal { color: #886aea; } .royal-bg { background-color: #886aea; } .royal-border { border-color: #6b46e5; } .dark, a.dark { color: #444; } .dark-bg { background-color: #444; } .dark-border { border-color: #111; } [collection-repeat] { /* Position is set by transforms */ left: 0 !important; top: 0 !important; position: absolute !important; z-index: 1; } .collection-repeat-container { position: relative; z-index: 1; } .collection-repeat-after-container { z-index: 0; display: block; /* when scrolling horizontally, make sure the after container doesn't take up 100% width */ } .collection-repeat-after-container.horizontal { display: inline-block; } [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak, .ng-hide:not(.ng-hide-animate) { display: none !important; } /** * Platform * -------------------------------------------------- * Platform specific tweaks */ .platform-ios.platform-cordova:not(.fullscreen) .bar-header:not(.bar-subheader) { height: 64px; } .platform-ios.platform-cordova:not(.fullscreen) .bar-header:not(.bar-subheader).item-input-inset .item-input-wrapper { margin-top: 19px !important; } .platform-ios.platform-cordova:not(.fullscreen) .bar-header:not(.bar-subheader) > * { margin-top: 20px; } .platform-ios.platform-cordova:not(.fullscreen) .tabs-top > .tabs, .platform-ios.platform-cordova:not(.fullscreen) .tabs.tabs-top { top: 64px; } .platform-ios.platform-cordova:not(.fullscreen) .has-header, .platform-ios.platform-cordova:not(.fullscreen) .bar-subheader { top: 64px; } .platform-ios.platform-cordova:not(.fullscreen) .has-subheader { top: 108px; } .platform-ios.platform-cordova:not(.fullscreen) .has-header.has-tabs-top { top: 113px; } .platform-ios.platform-cordova:not(.fullscreen) .has-header.has-subheader.has-tabs-top { top: 157px; } .platform-ios.platform-cordova .popover .bar-header:not(.bar-subheader) { height: 44px; } .platform-ios.platform-cordova .popover .bar-header:not(.bar-subheader).item-input-inset .item-input-wrapper { margin-top: -1px; } .platform-ios.platform-cordova .popover .bar-header:not(.bar-subheader) > * { margin-top: 0; } .platform-ios.platform-cordova .popover .has-header, .platform-ios.platform-cordova .popover .bar-subheader { top: 44px; } .platform-ios.platform-cordova .popover .has-subheader { top: 88px; } .platform-ios.platform-cordova.status-bar-hide { margin-bottom: 20px; } @media (orientation: landscape) { .platform-ios.platform-browser.platform-ipad { position: fixed; } } .platform-c:not(.enable-transitions) * { -webkit-transition: none !important; transition: none !important; } .slide-in-up { -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } .slide-in-up.ng-enter, .slide-in-up > .ng-enter { -webkit-transition: all cubic-bezier(0.1, 0.7, 0.1, 1) 400ms; transition: all cubic-bezier(0.1, 0.7, 0.1, 1) 400ms; } .slide-in-up.ng-enter-active, .slide-in-up > .ng-enter-active { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } .slide-in-up.ng-leave, .slide-in-up > .ng-leave { -webkit-transition: all ease-in-out 250ms; transition: all ease-in-out 250ms; } @-webkit-keyframes scaleOut { from { -webkit-transform: scale(1); opacity: 1; } to { -webkit-transform: scale(0.8); opacity: 0; } } @keyframes scaleOut { from { transform: scale(1); opacity: 1; } to { transform: scale(0.8); opacity: 0; } } @-webkit-keyframes superScaleIn { from { -webkit-transform: scale(1.2); opacity: 0; } to { -webkit-transform: scale(1); opacity: 1; } } @keyframes superScaleIn { from { transform: scale(1.2); opacity: 0; } to { transform: scale(1); opacity: 1; } } [nav-view-transition="ios"] [nav-view="entering"], [nav-view-transition="ios"] [nav-view="leaving"] { -webkit-transition-duration: 500ms; transition-duration: 500ms; -webkit-transition-timing-function: cubic-bezier(0.36, 0.66, 0.04, 1); transition-timing-function: cubic-bezier(0.36, 0.66, 0.04, 1); -webkit-transition-property: opacity, -webkit-transform, box-shadow; transition-property: opacity, transform, box-shadow; } [nav-view-transition="ios"][nav-view-direction="forward"], [nav-view-transition="ios"][nav-view-direction="back"] { background-color: #000; } [nav-view-transition="ios"] [nav-view="active"], [nav-view-transition="ios"][nav-view-direction="forward"] [nav-view="entering"], [nav-view-transition="ios"][nav-view-direction="back"] [nav-view="leaving"] { z-index: 3; } [nav-view-transition="ios"][nav-view-direction="back"] [nav-view="entering"], [nav-view-transition="ios"][nav-view-direction="forward"] [nav-view="leaving"] { z-index: 2; } [nav-bar-transition="ios"] .title, [nav-bar-transition="ios"] .buttons, [nav-bar-transition="ios"] .back-text { -webkit-transition-duration: 500ms; transition-duration: 500ms; -webkit-transition-timing-function: cubic-bezier(0.36, 0.66, 0.04, 1); transition-timing-function: cubic-bezier(0.36, 0.66, 0.04, 1); -webkit-transition-property: opacity, -webkit-transform; transition-property: opacity, transform; } [nav-bar-transition="ios"] [nav-bar="active"], [nav-bar-transition="ios"] [nav-bar="entering"] { z-index: 10; } [nav-bar-transition="ios"] [nav-bar="active"] .bar, [nav-bar-transition="ios"] [nav-bar="entering"] .bar { background: transparent; } [nav-bar-transition="ios"] [nav-bar="cached"] { display: block; } [nav-bar-transition="ios"] [nav-bar="cached"] .header-item { display: none; } [nav-view-transition="android"] [nav-view="entering"], [nav-view-transition="android"] [nav-view="leaving"] { -webkit-transition-duration: 200ms; transition-duration: 200ms; -webkit-transition-timing-function: cubic-bezier(0.4, 0.6, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0.6, 0.2, 1); -webkit-transition-property: -webkit-transform; transition-property: transform; } [nav-view-transition="android"] [nav-view="active"], [nav-view-transition="android"][nav-view-direction="forward"] [nav-view="entering"], [nav-view-transition="android"][nav-view-direction="back"] [nav-view="leaving"] { z-index: 3; } [nav-view-transition="android"][nav-view-direction="back"] [nav-view="entering"], [nav-view-transition="android"][nav-view-direction="forward"] [nav-view="leaving"] { z-index: 2; } [nav-bar-transition="android"] .title, [nav-bar-transition="android"] .buttons { -webkit-transition-duration: 200ms; transition-duration: 200ms; -webkit-transition-timing-function: cubic-bezier(0.4, 0.6, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0.6, 0.2, 1); -webkit-transition-property: opacity; transition-property: opacity; } [nav-bar-transition="android"] [nav-bar="active"], [nav-bar-transition="android"] [nav-bar="entering"] { z-index: 10; } [nav-bar-transition="android"] [nav-bar="active"] .bar, [nav-bar-transition="android"] [nav-bar="entering"] .bar { background: transparent; } [nav-bar-transition="android"] [nav-bar="cached"] { display: block; } [nav-bar-transition="android"] [nav-bar="cached"] .header-item { display: none; } [nav-swipe="fast"] [nav-view], [nav-swipe="fast"] .title, [nav-swipe="fast"] .buttons, [nav-swipe="fast"] .back-text { -webkit-transition-duration: 50ms; transition-duration: 50ms; -webkit-transition-timing-function: linear; transition-timing-function: linear; } [nav-swipe="slow"] [nav-view], [nav-swipe="slow"] .title, [nav-swipe="slow"] .buttons, [nav-swipe="slow"] .back-text { -webkit-transition-duration: 160ms; transition-duration: 160ms; -webkit-transition-timing-function: linear; transition-timing-function: linear; } [nav-view="cached"], [nav-bar="cached"] { display: none; } [nav-view="stage"] { opacity: 0; -webkit-transition-duration: 0; transition-duration: 0; } [nav-bar="stage"] .title, [nav-bar="stage"] .buttons, [nav-bar="stage"] .back-text { position: absolute; opacity: 0; -webkit-transition-duration: 0s; transition-duration: 0s; } .item-image i:last-child { position: absolute; border-radius: 50%; width: 20px; height: 20px; background-color: black; top: 15px; right: 15px; } .link-span { cursor: pointer; color: #387ef5; } .center { margin-left: auto; margin-right: auto; display: block; } p ul { list-style-type: disc !important; list-style-position: inside !important; } ================================================ FILE: mobile/www/css/style.css ================================================ /* General classes */ .pull-right { float:right; } .clearfix { clear:both; } .subheading, .date { font-size:12px; font-style:italic; } .center { margin-left: auto; margin-right: auto; display: block; } /* More specific to sections of app */ .profile-name .subheading, .profile-name .date { vertical-align: top; } div .tatami-avatar { min-height: 42px; } .tatami-attached-image{ height: auto; max-width: 350px; } .bar .bar-header .title { left:0!important; right:0!important; } .bar .bar-header .title a { text-decoration: none; } .bar .bar-header .title img { margin: 0 auto; vertical-align: middle; } /* Platform specific styles */ .platform-ios .tab-nav.tabs { bottom:43px; } .platform-ios .bar.bar-footer { bottom: 43px; !important; } .platform-ios .tatami-header { top: 64px; } .platform-ios .tatami-footer { bottom: 88px; } .platform-android .tatami-header { top: 88px; } .platform-android .tatami-footer { bottom: 44px; } .v-align { vertical-align: middle !important; } ================================================ FILE: mobile/www/i18n/en/conversation.json ================================================ { "conversation": { "title": "Conversation" } } ================================================ FILE: mobile/www/i18n/en/follow.json ================================================ { "tab": { "follower": "Followers", "following": "Following", "suggested": { "title": "Suggested", "who": "Who to Follow" } } } ================================================ FILE: mobile/www/i18n/en/home.json ================================================ { "tab": { "timeline": "Timeline", "mentions": "Mentions", "favorites": "Favorites", "more": "More" }, "noContent" : "No content found" } ================================================ FILE: mobile/www/i18n/en/login.json ================================================ { "login": { "title": "Login", "email": "E-mail", "password": "Password", "google": "Google Login", "success": "Logged In!", "failed": "Failed to log in!", "progress": "Login in progress...", "googleUnavaible": "Google Login unavailable", "tos": "Terms of Service", "accept": "Accept", "changeServer": "Change Tatami server", "endpoint": { "current": "Current Server:" } } } ================================================ FILE: mobile/www/i18n/en/more.json ================================================ { "more": { "title": "More Options", "company": "Company Timeline", "settings": "Settings", "blockedUsers": { "title": "Blocked Users", "no": "You don't have any blocked users" }, "reportedStatus":{ "title": "Reported Statuses", "no": "You don't have any reported statuses" }, "logout": "Logout", "language": { "change": "Language", "english": "English", "french": "French" }, "allUsers": "All Users" } } ================================================ FILE: mobile/www/i18n/en/post.json ================================================ { "post": { "title": "Post", "message": "Post Here...", "progress": "Post in progress...", "error": { "message": "Something went wrong. Returning to Timeline", "truncated": "Your post has been truncated to remain under the max length." }, "location": { "share": "You will share your location", "unshare": "You won't share your location", "fail": "Location failure. Please check that your GPS is enabled on your device" }, "private": { "yes": "Your message will be private", "no": "Your message will be public" } } } ================================================ FILE: mobile/www/i18n/en/server.json ================================================ { "server": { "success": "Logged In!", "failed": "Failed to log in!", "progress": "Login in progress...", "endpoint": { "title": "Server", "current": "Current Server:", "change": "Change Server", "error": { "title": "Error", "body": "Failed to change endpoint - cannot reach server." }, "authenticate": { "title": "Success", "body": "Endpoint changed - please reauthenticate with the new server." }, "default": "Use default server" } } } ================================================ FILE: mobile/www/i18n/en/status.json ================================================ { "status": { "share": { "title": "shared", "mention": "Your status has been shared", "toast": "You have shared this status", "action": "Share status" }, "reply": "In reply to ", "follower": "New follower", "delete": "Are you sure you want to delete this status?", "report" : "Report", "reportMessage": "This status has been reported", "reportStatus":{ "message": "Are you sure you want to report this status? This action cannot be undone.", "toast": "Status has been reported", "approve": "Are you sure you approve this status? It will continue to be displayed on the Company Timeline.", "approveToast": "Status has been approved", "delete": "Are you sure you want to delete this status? This action cannot be undone.", "deleteToast": "Status has been deleted" }, "block": { "reportStatus": "Report status", "blockUser": "Block user", "delete": "Delete" }, "announcement": { "title": "announced", "mention": "Your status has been announced", "toast": "You have announced this status", "action": "Announce status" }, "hide": { "action": "Hide status", "toast": "This status has been removed from your timeline" }, "favorite": { "add": "This status has been added to your favorites", "remove": "This status has been removed from your favorites" }, "private": "Private message", "noContent" : "No content found" } } ================================================ FILE: mobile/www/i18n/en/user.json ================================================ { "user": { "profile": { "title": "Profile", "options": { "block": "Block User", "deactivate": "Deactivate User", "reactivate": "Reactivate User" } }, "follow": "Follow", "unfollow": "Unfollow", "block":{ "success": "You have blocked this user", "title": "Block", "confirmation": "Are you sure you want to block this user? You won't be able to see their posts anymore." } , "unblock": { "success": "You have unblocked this user", "title": "Unblock" }, "reactivate": { "title": "Reactivate", "confirmation": "Are you sure you want to reactivate this user?" }, "deactivate": { "title": "Deactivate", "confirmation": "Are you sure you want to deactivate this user? They won't be able to log in to Tatami." } } } ================================================ FILE: mobile/www/i18n/fr/conversation.json ================================================ { "conversation": { "title": "Conversation" } } ================================================ FILE: mobile/www/i18n/fr/follow.json ================================================ { "tab": { "follower": "Abonnés", "following": "Abonnement", "suggested": { "title": "Recommandé", "who": "Qui suivre" } } } ================================================ FILE: mobile/www/i18n/fr/home.json ================================================ { "tab": { "timeline": "Timeline", "mentions": "Mentions", "favorites": "Favoris", "more": "Plus" }, "noContent" : "Cette section est actuellement vide" } ================================================ FILE: mobile/www/i18n/fr/login.json ================================================ { "login": { "title": "Login", "email": "E-mail", "password": "Mot de passe", "google": "Google Login", "success": "Connexion effectuée !", "failed": "Echec de connexion", "progress": "Connexion en cours...", "googleUnavaible": "Google Login indisponible", "tos": "conditions d'utilisation", "accept": "Accepter les", "changeServer": "Changer le serveur Tatami", "endpoint": { "current": "Serveur Actuel:" } } } ================================================ FILE: mobile/www/i18n/fr/more.json ================================================ { "more": { "title": "Plus d'options", "company": "Timeline de l'entreprise", "settings": "Paramètres", "blockedUsers": { "title": "Gérer vos utilisateurs bloqués", "no": "Vous n'avez bloqué aucun utilisateur" }, "reportedStatus": { "title": "Statuts signalés", "no": "Aucun statut n'a été signalé" }, "logout": "Déconnexion", "language": { "change": "Langue", "english": "Anglais", "french": "Français" }, "allUsers": "Tous les utilisateurs" } } ================================================ FILE: mobile/www/i18n/fr/post.json ================================================ { "post": { "title": "Post", "message": "Ecrivez votre message ici...", "progress": "Publication en cours...", "error": { "message": "Un problème est survenu. Retour à la Timeline.", "truncated": "Votre message a été tronqué afin de respecter la longueur maximale autorisée" }, "location": { "share": "Votre position sera partagée", "unshare": "Votre position ne sera pas partagée", "fail": "Echec de la localisation. Vérifiez que le GPS de votre téléphone est activé" }, "private": { "yes": "Votre message sera privé", "no": "Votre message sera public" } } } ================================================ FILE: mobile/www/i18n/fr/server.json ================================================ { "server": { "success": "Connexion effectuée !", "failed": "Echec de connexion", "progress": "Connexion en cours...", "endpoint": { "title": "Serveur", "current": "Serveur Actuel:", "change": "Changer de serveur", "error": { "title": "Échec de la modification du serveur", "body": "Serveur inaccessible." }, "authenticate": { "title": "Changement de serveur effectué", "body": "Veuillez-vous identifier avec le nouveau serveur." }, "default": "Utiliser le serveur par défaut" } } } ================================================ FILE: mobile/www/i18n/fr/status.json ================================================ { "status": { "share": { "title": "a partagé", "mention": "Votre statut a été partagé", "toast": "Vous avez partagé ce statut", "action": "Partager" }, "reply": "En réponse à ", "follower": "Nouvel abonné", "delete": "Êtes-vous sûr de vouloir supprimer ce statut ?", "report" : "Supprimer", "reportMessage": "Ce statut a été signalé", "reportStatus":{ "message": "Êtes-vous sûr de vouloir signaler ce statut?", "toast": "Vous avez signalé ce statut", "approve": "Êtes-vous sûr de vouloir approuver cette publication ? Cela ne modifiera pas son apparition sur Tatami.", "approveToast": "Vous avez approuvé ce statut", "delete": "Êtes-vous sûr de vouloir supprimer ce statut ? Cette action est irréversible.", "deleteToast": "Vous avez supprimé ce statut" }, "block": { "reportStatus": "Signaler ce statut", "blockUser": "Bloquer cet utilisateur", "delete": "Supprimer" }, "announcement": { "title": "a annoncé", "mention": "Votre status a été annoncé", "toast": "Vous avez annoncé ce statut", "action": "Annoncer" }, "hide": { "action": "Cacher ce statut", "toast": "Ce statut a été supprimé de votre timeline" }, "favorite": { "add": "Ce statut a été ajouté à vos favoris", "remove": "Ce statut a été supprimé de vos favoris" }, "private": "Message privé", "noContent" : "Cette section est actuellement vide" } } ================================================ FILE: mobile/www/i18n/fr/user.json ================================================ { "user": { "profile": { "title": "Profil", "options": { "block": "Bloquer cet utilisateur", "deactivate": "Désactiver cet utilisateur", "reactivate": "Réactiver cet utilisateur" } }, "follow": "S'abonner", "unfollow": "Se désabonner", "block":{ "success": "Vous avez bloqué cet utilisateur", "title": "Bloquer", "confirmation": "Êtes-vous sûr de vouloir bloquer cet utilisateur ? Vous ne verrez plus ses publications." } , "unblock": { "success": "Vous avez débloqué cet utilisateur.", "title": "Débloquer" }, "reactivate": { "title": "Réactiver", "confirmation": "Êtes-vous sûr de vouloir réactiver cet utilisateur ?" }, "deactivate": { "title": "Désactiver", "confirmation": "Êtes-vous sûr de vouloir désactiver cet utilisateur ? Il ne pourra plus se connecter à Tatami." } } } ================================================ FILE: mobile/www/index.html ================================================ ================================================ FILE: mobile/www/test/javascript/components/profile/profile.controller.spec.js ================================================ describe("License controller test", function() { beforeEach(inject(function(_$controller_) { $controller = _$controller_; })); it('SANITY', function(){ expect(true).toBeTruthy(); }); }); ================================================ FILE: pom.xml ================================================ 4.0.0 fr.ippon.tatami tatami 4.0.5 web services tatamibot mobile pom Tatami is an enterprise micro-blogging platform 2012 https://github.com/ippontech/tatami Ippon Technologies http://www.ippon.fr Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 repo GitHub https://github.com/ippontech/tatami/issues http://github.com/ippontech/tatami scm:git:git@github.com:ippontech/tatami.git default true elasticsearch-remote remote target/elasticsearch target/elasticsearch/log true preprod apple-push 0 DEBUG PAPERTRAIL logs.papertrailapp.com 38143 128M http://sandbox.tatamisoft.com localhost 25 tatami@ippon.fr true false prod 80 0 INFO PAPERTRAIL logs.papertrailapp.com 38143 512M http://tatami.ippon.fr ldap://10.55.0.4:389 localhost 25 tatami@ippon.fr UA-10959780-3 true false https embedded /opt/tatami/data/elasticsearch /opt/tatami/log/elasticsearch false false 2003 org.mortbay.jetty jetty-maven-plugin ${maven.jetty.version} ${jetty.scanIntervalSeconds} stop-jetty 9999 jetty.port ${jetty.port} start-jetty pre-integration-test run 0 true stop-jetty post-integration-test stop org.codehaus.mojo exec-maven-plugin 1.2.1 insert-version generate-sources scripts/insertBuildVersion.sh ${project.version} ${buildNumber} exec stress-tests 0 WARN CONSOLE 64M true uitest services/test/resources src/integration/resources org.codehaus.mojo build-helper-maven-plugin 1.4 add-uitests-source add-test-source ${basedir}/src/integration/java org.codehaus.gmaven gmaven-plugin 1.4 1.8 ${basedir}/src/integration/java **/*.groovy testCompile org.apache.maven.plugins maven-failsafe-plugin 2.12.3 ${basedir}/src/integration/java **/*Spec.* target/test-reports/geb ${webdriver.chrome.driver} ${google.password} ${google.email} integration-test verify org.codehaus.mojo cassandra-maven-plugin ${maven.cassandra.version} start-cassandra pre-integration-test start org.codehaus.groovy groovy-all 1.8.1 org.spockframework spock-core 0.6-groovy-1.8 test org.codehaus.geb geb-spock 0.7.1 test org.seleniumhq.selenium selenium-firefox-driver ${selenium.version} test org.seleniumhq.selenium selenium-htmlunit-driver ${selenium.version} test org.seleniumhq.selenium selenium-chrome-driver ${selenium.version} test org.seleniumhq.selenium selenium-support ${selenium.version} test org.apache.directory.server apacheds-all 1.5.5 test org.apache.directory.shared shared-ldap 8080 1 DEBUG CONSOLE localhost 64M ${project.version} http://localhost:8080 ldap://directory:389 dc=ippon,dc=fr (uid={0}) tatami@localhost false true any embedded target/elasticsearch target/elasticsearch/log true false 2003 2592000 ${project.basedir}/node_modules/.bin ${project.build.directory}/test-results reuseReports jacoco ${project.testresult.directory}/surefire-reports ${project.testresult.directory}/coverage/jacoco/jacoco.exec ${project.testresult.directory}/coverage/jacoco/jacoco-it.exec ${project.basedir}/src/main/ ${project.basedir}/src/test/ ${project.testresult.directory}/coverage/report-lcov/lcov.info ${project.testresult.directory}/karma src/main/webapp/assets/**.* UTF-8 1.6 3.2.3.RELEASE 3.1.3.RELEASE 0.20.6 1.6.11 1.7.5 1.0.13 4.2.2 4.2.2 1.2.6 1.1-4 3.1-09 1.0.2 1 3.0.1 1.0.0.GA 4.3.0.Final 1.2 2.1.2 2.1 2.6 2.3 4.11 1.3 1.3.5 1.2.0.1 1.9.0 1.0 1.2.1 1.7 2.2.0 2.11.0 2.31.0 3.0 1.2 2.14 2.3 8.1.13.v20130916 1.2.1-1 1.7.0 2.9 Tomcat sonatype-releases Sonatype Releases Repository http://oss.sonatype.org/content/repositories/releases/ sonatype-nexus-snapshots Sonatype Nexus Snapshots https://oss.sonatype.org/content/repositories/snapshots false true 3.1.0 com.google.api-client google-api-client 1.20.0 com.google.apis google-api-services-plus v1-rev345-1.21.0 com.fasterxml.jackson.datatype jackson-datatype-json-org ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-hppc ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-joda ${jackson.version} com.notnoop.apns apns 0.2.3 com.yammer.metrics metrics-core ${yammer.metrics.version} com.yammer.metrics metrics-ehcache ${yammer.metrics.version} com.yammer.metrics metrics-graphite ${yammer.metrics.version} com.yammer.metrics metrics-servlet ${yammer.metrics.version} com.yammer.metrics metrics-spring ${yammer.metrics.version} com.yammer.metrics metrics-web ${yammer.metrics.version} commons-fileupload commons-fileupload 1.2.2 commons-io commons-io ${commons.io.version} commons-lang commons-lang ${commons.lang.version} javax.inject javax.inject ${javax.inject.version} javax.persistence persistence-api ${persistence.api.version} javax.servlet javax.servlet-api ${javax.servlet.api.version} provided javax.servlet jstl ${jstl.version} javax.validation validation-api ${javax.validation.api.version} joda-time joda-time ${jodatime.version} ch.qos.logback logback-core ${logback.version} ch.qos.logback logback-classic ${logback.version} net.sf.ehcache ehcache-core 2.6.5 net.sf.ehcache ehcache-web 2.0.4 org.apache.camel camel-core ${camel.version} org.apache.camel camel-spring ${camel.version} org.apache.camel camel-script ${camel.version} org.apache.camel camel-rss ${camel.version} org.apache.camel camel-twitter ${camel.version} org.apache.camel camel-stream ${camel.version} org.apache.httpcomponents httpcore ${httpclient.core.version} org.apache.httpcomponents httpclient ${httpclient.client.version} org.apache.geronimo.javamail geronimo-javamail_1.4_mail 1.8.2 org.apache.openjpa openjpa 2.1.0 org.apache.velocity velocity ${velocity.version} org.aspectj aspectjrt ${aspectj.version} jar compile org.aspectj aspectjweaver ${aspectj.version} jar compile org.atmosphere atmosphere-annotations 1.1.0.RC3 org.elasticsearch elasticsearch ${elasticsearch.version} org.hectorclient hector-core ${hector.version} javax.servlet servlet-api org.hectorclient hector-object-mapper ${hector.mapper.version} org.mortbay.jetty servlet-api org.hibernate hibernate-validator ${hibernate.validator.version} org.pegdown pegdown ${pegdown.version} org.slf4j slf4j-api ${slf4j.version} org.slf4j jcl-over-slf4j ${slf4j.version} org.springframework spring-aop ${spring.version} org.springframework spring-beans ${spring.version} org.springframework spring-context ${spring.version} org.springframework spring-context-support ${spring.version} org.springframework spring-core ${spring.version} org.springframework spring-orm ${spring.version} org.springframework spring-tx ${spring.version} org.springframework spring-web ${spring.version} org.springframework spring-webmvc ${spring.version} org.springframework.security spring-security-core ${spring.security.version} org.springframework.security spring-security-config ${spring.security.version} org.springframework.security spring-security-crypto ${spring.security.version} org.springframework.security spring-security-ldap ${spring.security.version} org.springframework.security spring-security-openid ${spring.security.version} org.springframework.security spring-security-taglibs ${spring.security.version} org.springframework.security spring-security-web ${spring.security.version} org.pac4j pac4j-oauth 1.6.0 org.pac4j spring-security-pac4j 1.2.4 rome rome ${rome.version} org.apache.cassandra cassandra-all ${cassandra.version} javax.servlet servlet-api org.mortbay.jetty servlet-api org.mortbay.jetty jetty org.mortbay.jetty jetty-util test com.jayway.jsonpath json-path 0.8.1 test org.springframework spring-test ${spring.version} test org.hamcrest hamcrest-core ${hamcrest.version} test org.hamcrest hamcrest-library ${hamcrest.version} test junit junit ${junit.version} test com.jayway.awaitility awaitility ${awaitility.version} test org.cassandraunit cassandra-unit ${cassandra.unit.version} test hamcrest-all org.hamcrest org.mockito mockito-all ${mockito.version} test org.apache.camel camel-test ${camel.version} test root src/main/resources true **/* org.apache.maven.plugins maven-compiler-plugin ${maven.compiler.version} ${java.version} ${java.version} org.apache.maven.plugins maven-enforcer-plugin ${maven.enforcer.version} enforce-versions enforce [3.0.0,) [1.6.0,) org.codehaus.mojo buildnumber-maven-plugin 1.1 7 validate create org.apache.maven.plugins maven-surefire-plugin ${maven.surefire.version} ${project.testresult.directory}/surefire-reports -XX:MaxPermSize=128m -Xmx256m ${surefireArgLine} ${skip.unit.tests} alphabetical org.codehaus.mojo cassandra-maven-plugin ${maven.cassandra.version} org.apache.cassandra cassandra-all ${cassandra.version} org.mortbay.jetty jetty-maven-plugin ${maven.jetty.version} ${jetty.scanIntervalSeconds} stop-jetty 9999 jetty.port ${jetty.port} spring.profiles.active ${spring.profiles.active} / org.apache.tomcat.maven tomcat7-maven-plugin 2.1 / true org.apache.coyote.http11.Http11NioProtocol ${spring.profiles.active} org.apache.maven.plugins maven-eclipse-plugin ${maven-eclipse-plugin.version} true true .settings/org.eclipse.core.resources.prefs =${project.build.sourceEncoding}${line.separator}]]> org.codehaus.mojo sonar-maven-plugin 2.5 org.jacoco jacoco-maven-plugin 0.7.4.201502262128 pre-unit-tests prepare-agent ${project.testresult.directory}/coverage/jacoco/jacoco.exec surefireArgLine post-unit-test test report ${project.testresult.directory}/coverage/jacoco/jacoco.exec ${project.testresult.directory}/coverage/jacoco org.eclipse.m2e lifecycle-mapping 1.0.0 ro.isdc.wro4j wro4j-maven-plugin [1.4.7,) run org.codehaus.gmaven gmaven-plugin [1.4,) testCompile ================================================ FILE: scripts/insertBuildVersion.sh ================================================ #!/bin/bash darwin=false; case "`uname`" in Darwin*) darwin=true ;; esac if $darwin; then sedi="sed -i .sed.del" else sedi="sed -i" fi find . -name FooterView.html | xargs $sedi "s/version<\/pom>/$1/" find . -name FooterView.html | xargs $sedi "s/build<\/pom>/$2/" find . -name HomeSidebarView.html | xargs $sedi "s/version<\/pom>/$1/" find . -name HomeSidebarView.html | xargs $sedi "s/build<\/pom>/$2/" if $darwin; then find . -name FooterView.html.sed.del | xargs rm find . -name HomeSidebarView.html.sed.del | xargs rm fi ================================================ FILE: services/pom.xml ================================================ tatami fr.ippon.tatami 4.0.5 4.0.0 services tatami-services-${project.version} org.apache.maven.plugins maven-jar-plugin 2.4 test-jar org.apache.maven.plugins maven-remote-resources-plugin 1.5 bundle **/*.xml **/tatami/**/* ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/ApplicationConfiguration.java ================================================ package fr.ippon.tatami.config; import org.apache.thrift.transport.TTransportException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.*; import org.springframework.core.env.Environment; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.io.IOException; @Configuration @PropertySource({"classpath:/META-INF/tatami/tatami.properties", "classpath:/META-INF/tatami/customization.properties"}) @ComponentScan(basePackages = { "fr.ippon.tatami.repository", "fr.ippon.tatami.service", "fr.ippon.tatami.security"}) @Import(value = { AsyncConfiguration.class, CacheConfiguration.class, CassandraConfiguration.class, SearchConfiguration.class, MailConfiguration.class, MetricsConfiguration.class}) @ImportResource("classpath:META-INF/spring/applicationContext-*.xml") public class ApplicationConfiguration { private final Logger log = LoggerFactory.getLogger(ApplicationConfiguration.class); @Inject private Environment env; /** * Initializes Tatami. *

    * Spring profiles can be configured with a system property -Dspring.profiles.active=your-active-profile *

    * Available profiles are : * - "apple-push" : for enabling Apple Push notifications * - "metrics" : for enabling Yammer Metrics * - "tatamibot" : for enabling the Tatami bot */ @PostConstruct public void initTatami() throws IOException, TTransportException { log.debug("Looking for Spring profiles... Available profiles are \"metrics\", \"tatamibot\" and \"apple-push\""); if (env.getActiveProfiles().length == 0) { log.debug("No Spring profile configured, running with default configuration"); } else { for (String profile : env.getActiveProfiles()) { log.debug("Detected Spring profile : " + profile); } } Constants.VERSION = env.getRequiredProperty("tatami.version"); Constants.GOOGLE_ANALYTICS_KEY = env.getProperty("tatami.google.analytics.key"); log.info("Tatami v. {} started!", Constants.VERSION); log.debug("Google Analytics key : {}", Constants.GOOGLE_ANALYTICS_KEY); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/AsyncConfiguration.java ================================================ package fr.ippon.tatami.config; import fr.ippon.tatami.service.SearchService; import fr.ippon.tatami.service.elasticsearch.ElasticsearchSearchService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync @EnableScheduling public class AsyncConfiguration implements AsyncConfigurer { private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class); @Bean public SearchService searchService() { return new ElasticsearchSearchService(); } @Override public Executor getAsyncExecutor() { log.debug("Creating Async Task Executor"); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(50); executor.setQueueCapacity(10000); executor.setThreadNamePrefix("TatamiExecutor-"); executor.initialize(); return executor; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/CacheConfiguration.java ================================================ package fr.ippon.tatami.config; import com.yammer.metrics.ehcache.InstrumentedEhcache; import net.sf.ehcache.Cache; import net.sf.ehcache.Ehcache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.ehcache.EhCacheCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import javax.annotation.PreDestroy; import javax.inject.Inject; @Configuration @EnableCaching public class CacheConfiguration { private final Logger log = LoggerFactory.getLogger(CacheConfiguration.class); private net.sf.ehcache.CacheManager cacheManager; @Inject private Environment env; @PreDestroy public void destroy() { log.info("Closing Ehcache"); cacheManager.shutdown(); } @Bean public CacheManager cacheManager() { cacheManager = new net.sf.ehcache.CacheManager(); if (env.acceptsProfiles(Constants.SPRING_PROFILE_METRICS)) { log.debug("Ehcache Metrics monitoring enabled"); Cache statusCache = cacheManager.getCache("status-cache"); Ehcache decoratedStatusCache = InstrumentedEhcache.instrument(statusCache); cacheManager.replaceCacheWithDecoratedCache(statusCache, decoratedStatusCache); Cache userCache = cacheManager.getCache("user-cache"); Ehcache decoratedUserCache = InstrumentedEhcache.instrument(userCache); cacheManager.replaceCacheWithDecoratedCache(userCache, decoratedUserCache); Cache attachmentCache = cacheManager.getCache("attachment-cache"); Ehcache decoratedAttachmentCache = InstrumentedEhcache.instrument(attachmentCache); cacheManager.replaceCacheWithDecoratedCache(attachmentCache, decoratedAttachmentCache); Cache friendsCache = cacheManager.getCache("friends-cache"); Ehcache decoratedFriendsCache = InstrumentedEhcache.instrument(friendsCache); cacheManager.replaceCacheWithDecoratedCache(friendsCache, decoratedFriendsCache); Cache followersCache = cacheManager.getCache("followers-cache"); Ehcache decoratedFollowersCache = InstrumentedEhcache.instrument(followersCache); cacheManager.replaceCacheWithDecoratedCache(followersCache, decoratedFollowersCache); Cache groupCache = cacheManager.getCache("group-cache"); Ehcache decoratedGroupCache = InstrumentedEhcache.instrument(groupCache); cacheManager.replaceCacheWithDecoratedCache(groupCache, decoratedGroupCache); Cache groupUserCache = cacheManager.getCache("group-user-cache"); Ehcache decoratedGroupUserCache = InstrumentedEhcache.instrument(groupUserCache); cacheManager.replaceCacheWithDecoratedCache(groupUserCache, decoratedGroupUserCache); } EhCacheCacheManager ehCacheManager = new EhCacheCacheManager(); ehCacheManager.setCacheManager(cacheManager); return ehCacheManager; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/CassandraConfiguration.java ================================================ package fr.ippon.tatami.config; import me.prettyprint.cassandra.connection.HOpTimer; import me.prettyprint.cassandra.connection.MetricsOpTimer; import me.prettyprint.cassandra.model.ConfigurableConsistencyLevel; import me.prettyprint.cassandra.service.CassandraHostConfigurator; import me.prettyprint.cassandra.service.ThriftCfDef; import me.prettyprint.cassandra.service.ThriftCluster; import me.prettyprint.cassandra.service.ThriftKsDef; import me.prettyprint.hector.api.Cluster; import me.prettyprint.hector.api.HConsistencyLevel; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.ddl.ColumnFamilyDefinition; import me.prettyprint.hector.api.ddl.ComparatorType; import me.prettyprint.hector.api.ddl.KeyspaceDefinition; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hom.EntityManagerImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import javax.annotation.PreDestroy; import javax.inject.Inject; import java.util.ArrayList; import java.util.List; /** * Cassandra configuration file. * * @author Julien Dubois */ @Configuration public class CassandraConfiguration { private final Logger log = LoggerFactory.getLogger(CassandraConfiguration.class); @Inject private Environment env; private Cluster myCluster; @PreDestroy public void destroy() { log.info("Closing Hector connection pool"); myCluster.getConnectionManager().shutdown(); HFactory.shutdownCluster(myCluster); } @Bean public Keyspace keyspaceOperator() { log.info("Configuring Cassandra keyspace"); String cassandraHost = env.getProperty("cassandra.host"); String cassandraClusterName = env.getProperty("cassandra.clusterName"); String cassandraKeyspace = env.getProperty("cassandra.keyspace"); CassandraHostConfigurator cassandraHostConfigurator = new CassandraHostConfigurator(cassandraHost); cassandraHostConfigurator.setMaxActive(100); if (env.acceptsProfiles(Constants.SPRING_PROFILE_METRICS)) { log.debug("Cassandra Metrics monitoring enabled"); HOpTimer hOpTimer = new MetricsOpTimer(cassandraClusterName); cassandraHostConfigurator.setOpTimer(hOpTimer); } ThriftCluster cluster = new ThriftCluster(cassandraClusterName, cassandraHostConfigurator); this.myCluster = cluster; // Keep a pointer to the cluster, as Hector is buggy and can't find it again... ConfigurableConsistencyLevel consistencyLevelPolicy = new ConfigurableConsistencyLevel(); consistencyLevelPolicy.setDefaultReadConsistencyLevel(HConsistencyLevel.ONE); KeyspaceDefinition keyspaceDef = cluster.describeKeyspace(cassandraKeyspace); if (keyspaceDef == null) { log.warn("Keyspace \" {} \" does not exist, creating it!", cassandraKeyspace); keyspaceDef = new ThriftKsDef(cassandraKeyspace); cluster.addKeyspace(keyspaceDef, true); addColumnFamily(cluster, ColumnFamilyKeys.USER_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.FRIENDS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.FOLLOWERS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.STATUS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.DOMAIN_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.REGISTRATION_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.RSS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.MAILDIGEST_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.SHARES_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.DISCUSSION_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.USER_TAGS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.TAG_FOLLOWERS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.GROUP_MEMBERS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.USER_GROUPS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.GROUP_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.GROUP_DETAILS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.ATTACHMENT_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.AVATAR_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.DOMAIN_CONFIGURATION_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.TATAMIBOT_CONFIGURATION_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.APPLE_DEVICE_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.BLOCK_USERS_CF, 0); addColumnFamily(cluster, ColumnFamilyKeys.STATUS_REPORT_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.TIMELINE_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.TIMELINE_SHARES_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.MENTIONLINE_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.USERLINE_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.USERLINE_SHARES_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.FAVLINE_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.TAGLINE_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.TRENDS_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.USER_TRENDS_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.GROUPLINE_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.USER_ATTACHMENT_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.STATUS_ATTACHMENT_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.DOMAINLINE_CF, 0); addColumnFamilySortedbyUUID(cluster, ColumnFamilyKeys.DOMAIN_TATAMIBOT_CF, 0); addColumnFamilyCounter(cluster, ColumnFamilyKeys.COUNTER_CF, 0); addColumnFamilyCounter(cluster, ColumnFamilyKeys.TAG_COUNTER_CF, 0); addColumnFamilyCounter(cluster, ColumnFamilyKeys.GROUP_COUNTER_CF, 0); addColumnFamilyCounter(cluster, ColumnFamilyKeys.DAYLINE_CF, 0); //Tatami Bot CF addColumnFamily(cluster, ColumnFamilyKeys.TATAMIBOT_DUPLICATE_CF, 0); } else { /* This case only concerns the creation of new CF that didn't exist in the previous version of Tatami. As we cannot afford to drop the keyspace in production, this case is an update of the database that should be executed only one time in production. */ List lcf = keyspaceDef.getCfDefs(); List lcfNames = new ArrayList(); for(ColumnFamilyDefinition cfd : lcf){ lcfNames.add(cfd.getName()); } // The new tables if(!lcfNames.contains(ColumnFamilyKeys.BLOCK_USERS_CF)){ addColumnFamily(cluster, ColumnFamilyKeys.BLOCK_USERS_CF, 0); log.debug("{} column family successfully created", ColumnFamilyKeys.BLOCK_USERS_CF); } if(!lcfNames.contains(ColumnFamilyKeys.STATUS_REPORT_CF)){ addColumnFamily(cluster, ColumnFamilyKeys.STATUS_REPORT_CF, 0); log.debug("{} column family successfully created", ColumnFamilyKeys.STATUS_REPORT_CF); } } return HFactory.createKeyspace(cassandraKeyspace, cluster, consistencyLevelPolicy); } @Bean public EntityManagerImpl entityManager(Keyspace keyspace) { String[] packagesToScan = {"fr.ippon.tatami.domain", "fr.ippon.tatami.bot.config"}; return new EntityManagerImpl(keyspace, packagesToScan); } private void addColumnFamily(ThriftCluster cluster, String cfName, int rowCacheKeysToSave) { String cassandraKeyspace = this.env.getProperty("cassandra.keyspace"); ColumnFamilyDefinition cfd = HFactory.createColumnFamilyDefinition(cassandraKeyspace, cfName); cfd.setRowCacheKeysToSave(rowCacheKeysToSave); cluster.addColumnFamily(cfd); } private void addColumnFamilySortedbyUUID(ThriftCluster cluster, String cfName, int rowCacheKeysToSave) { String cassandraKeyspace = this.env.getProperty("cassandra.keyspace"); ColumnFamilyDefinition cfd = HFactory.createColumnFamilyDefinition(cassandraKeyspace, cfName); cfd.setRowCacheKeysToSave(rowCacheKeysToSave); cfd.setComparatorType(ComparatorType.UUIDTYPE); cluster.addColumnFamily(cfd); } private void addColumnFamilyCounter(ThriftCluster cluster, String cfName, int rowCacheKeysToSave) { String cassandraKeyspace = this.env.getProperty("cassandra.keyspace"); ThriftCfDef cfd = new ThriftCfDef(cassandraKeyspace, cfName, ComparatorType.UTF8TYPE); cfd.setRowCacheKeysToSave(rowCacheKeysToSave); cfd.setDefaultValidationClass(ComparatorType.COUNTERTYPE.getClassName()); cluster.addColumnFamily(cfd); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/ColumnFamilyKeys.java ================================================ package fr.ippon.tatami.config; /** * @author Julien Dubois */ public class ColumnFamilyKeys { private ColumnFamilyKeys() { } public static final String USER_CF = "User"; public static final String FRIENDS_CF = "Friends"; public static final String FOLLOWERS_CF = "Followers"; public static final String STATUS_CF = "Status"; public static final String SHARES_CF = "Shares"; public static final String DISCUSSION_CF = "Discussion"; public static final String DAYLINE_CF = "Dayline"; public static final String FAVLINE_CF = "Favline"; public static final String TAGLINE_CF = "Tagline"; public static final String TIMELINE_CF = "Timeline"; public static final String TIMELINE_SHARES_CF = "TimelineShares"; public static final String MENTIONLINE_CF = "Mentionline"; public static final String USERLINE_CF = "Userline"; public static final String USERLINE_SHARES_CF = "UserlineShares"; public static final String COUNTER_CF = "Counter"; public static final String DOMAIN_CF = "Domain"; public static final String REGISTRATION_CF = "Registration"; public static final String RSS_CF = "Rss"; public static final String MAILDIGEST_CF = "MailDigest"; public static final String TRENDS_CF = "Trends"; public static final String TAG_FOLLOWERS_CF = "TagFollowers"; public static final String USER_TAGS_CF = "UserTags"; public static final String TAG_COUNTER_CF = "TagCounter"; public static final String USER_TRENDS_CF = "UserTrends"; public static final String GROUP_CF = "Group"; public static final String GROUP_DETAILS_CF = "GroupDetails"; public static final String GROUP_MEMBERS_CF = "GroupMembers"; public static final String USER_GROUPS_CF = "UserGroups"; public static final String GROUP_COUNTER_CF = "GroupCounter"; public static final String GROUPLINE_CF = "Groupline"; public static final String TATAMIBOT_DUPLICATE_CF = "TatamiBotDuplicate"; public static final String ATTACHMENT_CF = "Attachment"; public static final String USER_ATTACHMENT_CF = "UserAttachments"; public static final String STATUS_ATTACHMENT_CF = "StatusAttachments"; public static final String DOMAIN_CONFIGURATION_CF = "DomainConfiguration"; public static final String DOMAINLINE_CF = "Domainline"; public static final String DOMAIN_TATAMIBOT_CF = "DomainTatamibot"; public static final String TATAMIBOT_CONFIGURATION_CF = "TatamibotConfiguration"; public static final String AVATAR_CF = "Avatar"; public static final String APPLE_DEVICE_CF = "AppleDevice"; public static final String APPLE_DEVICE_USER_CF = "AppleDeviceUser"; public static final String BLOCK_USERS_CF = "UsersBlocked"; public static final String STATUS_REPORT_CF = "ReportedStatus"; } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/Constants.java ================================================ package fr.ippon.tatami.config; /** * Application constants. */ public class Constants { private Constants() { } public static final String SPRING_PROFILE_METRICS = "metrics"; public static final String REMOTE_ENGINE = "remote"; public static final String EMBEDDED_ENGINE = "embedded"; public static String VERSION = null; public static String GOOGLE_ANALYTICS_KEY = null; public static final int PAGINATION_SIZE = 50; public static final String TATAMIBOT_NAME = "tatamibot"; public static final int AVATAR_SIZE = 200; /** * Cassandra : number of columns to return when not doing a name-based template */ public static final int CASSANDRA_MAX_COLUMNS = 10000; /** * Cassandra : number of rows to return */ public static final int CASSANDRA_MAX_ROWS = 10000; } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/DispatcherServletConfig.java ================================================ package fr.ippon.tatami.config; import fr.ippon.tatami.web.syndic.SyndicView; import org.apache.commons.lang.CharEncoding; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import org.springframework.context.annotation.*; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.env.Environment; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.commons.CommonsMultipartResolver; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.SessionLocaleResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; import org.springframework.web.servlet.view.JstlView; import org.springframework.web.servlet.view.UrlBasedViewResolver; import org.springframework.web.servlet.view.json.MappingJackson2JsonView; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; @Configuration @ComponentScan("fr.ippon.tatami.web") @EnableWebMvc @PropertySource({"classpath:/META-INF/tatami/tatami.properties", "classpath:/META-INF/tatami/customization.properties"}) @ImportResource("classpath:META-INF/spring/applicationContext-metrics.xml") public class DispatcherServletConfig extends WebMvcConfigurerAdapter { private final Logger log = LoggerFactory.getLogger(DispatcherServletConfig.class); @Inject private Environment env; @Bean public ViewResolver ContentNegotiatingViewResolver() { log.debug("Configuring the ContentNegotiatingViewResolver"); ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver(); List viewResolvers = new ArrayList(); UrlBasedViewResolver urlBasedViewResolver = new UrlBasedViewResolver(); urlBasedViewResolver.setViewClass(JstlView.class); urlBasedViewResolver.setPrefix("/WEB-INF/pages/"); urlBasedViewResolver.setSuffix(".jsp"); viewResolvers.add(urlBasedViewResolver); viewResolver.setViewResolvers(viewResolvers); List defaultViews = new ArrayList(); defaultViews.add(new MappingJackson2JsonView()); defaultViews.add(syndicView()); viewResolver.setDefaultViews(defaultViews); return viewResolver; } @Bean public SyndicView syndicView() { return new SyndicView(); } @Bean public SessionLocaleResolver localeResolver() { return new SessionLocaleResolver(); } @Bean public LocaleChangeInterceptor localeChangeInterceptor() { log.debug("Configuring localeChangeInterceptor"); LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); localeChangeInterceptor.setParamName("language"); return localeChangeInterceptor; } @Bean public MessageSource messageSource() { log.debug("Loading MessageSources"); ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("/WEB-INF/messages/messages"); messageSource.setDefaultEncoding(CharEncoding.UTF_8); if ("true".equals(env.getProperty("tatami.message.reloading.enabled"))) { messageSource.setCacheSeconds(1); } return messageSource; } @Bean public MultipartResolver multipartResolver() { CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); Long maxSize = Long.parseLong(env.getProperty("file.max.size")); multipartResolver.setMaxUploadSize(maxSize); // 10 Mo max file size by default return multipartResolver; } @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping() { log.debug("Creating requestMappingHandlerMapping"); RequestMappingHandlerMapping requestMappingHandlerMapping = new RequestMappingHandlerMapping(); requestMappingHandlerMapping.setUseSuffixPatternMatch(false); Object[] interceptors = {localeChangeInterceptor()}; requestMappingHandlerMapping.setInterceptors(interceptors); return requestMappingHandlerMapping; } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { log.debug("Adding static resource handlers"); registry.addResourceHandler("/static-wro4j/" + env.getProperty("tatami.version") + "/**") .addResourceLocations("/WEB-INF/generated-wro4j/") .setCachePeriod(60 * 60 * 24 * 30); registry.addResourceHandler("/css/**") .addResourceLocations("/css/") .setCachePeriod(60 * 60 * 24 * 30); registry.addResourceHandler("/static/" + env.getProperty("tatami.version") + "/**") .addResourceLocations("/js/") .setCachePeriod(60 * 60 * 24 * 30); } @Override public void configureHandlerExceptionResolvers(List exceptionResolvers) { exceptionResolvers.add(new HandlerExceptionResolver() { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (log.isErrorEnabled()) { log.error("An error has occured : " + ex.getMessage()); ex.printStackTrace(); } response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return new ModelAndView(); } catch (Exception handlerException) { log.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException); } return null; } }); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/GroupRoles.java ================================================ package fr.ippon.tatami.config; /** * Constants for groupe roles. */ public class GroupRoles { private GroupRoles() { } public static final String ADMIN = "ADMIN"; public static final String MEMBER = "MEMBER"; } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/MailConfiguration.java ================================================ package fr.ippon.tatami.config; import org.apache.commons.lang.CharEncoding; import org.apache.velocity.app.VelocityEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.ui.velocity.VelocityEngineFactoryBean; import java.io.IOException; import java.util.Properties; /** * Configuration for velocity template and i18n for emails. * * @author Pierre Rust */ @Configuration public class MailConfiguration { private static final Logger log = LoggerFactory.getLogger(MailConfiguration.class); @Bean public VelocityEngine velocityEngine() throws IOException { log.debug("Starting Velocity Engine"); VelocityEngineFactoryBean factory = new VelocityEngineFactoryBean(); Properties props = new Properties(); props.put("resource.loader", "class"); props.put("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); // necessary to get logs on templates's error props.put("runtime.log.logsystem.class", "org.apache.velocity.runtime.log.Log4JLogChute"); props.put("runtime.log.error.stacktrace", "true"); props.put("runtime.log.warn.stacktrace", "true"); props.put("runtime.log.info.stacktrace", "true"); props.put("runtime.log.invalid.reference", "true"); // TODO : FileResourceLoader could be used to externalize templates // enable relative includes props.put("eventhandler.include.class", "org.apache.velocity.app.event.implement.IncludeRelativePath"); factory.setVelocityProperties(props); return factory.createVelocityEngine(); } @Bean public MessageSource mailMessageSource() { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:/META-INF/tatami/mails/messages/messages"); messageSource.setDefaultEncoding(CharEncoding.UTF_8); log.info("loading non-reloadable mail messages resources"); return messageSource; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/MetricsConfiguration.java ================================================ package fr.ippon.tatami.config; import com.yammer.metrics.HealthChecks; import com.yammer.metrics.reporting.GraphiteReporter; import fr.ippon.tatami.config.metrics.CassandraHealthCheck; import fr.ippon.tatami.config.metrics.JavaMailHealthCheck; import fr.ippon.tatami.service.MailService; import me.prettyprint.hector.api.Keyspace; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.concurrent.TimeUnit; @Configuration public class MetricsConfiguration { private final Logger log = LoggerFactory.getLogger(MetricsConfiguration.class); @Inject private Environment env; @Inject private Keyspace keyspaceOperator; @Inject private MailService mailService; @PostConstruct public void initMetrics() { if (env.acceptsProfiles(Constants.SPRING_PROFILE_METRICS)) { log.debug("Initializing Metrics healthchecks"); HealthChecks.register(new CassandraHealthCheck(keyspaceOperator)); HealthChecks.register(new JavaMailHealthCheck(mailService)); String graphiteHost = env.getProperty("tatami.metrics.graphite.host"); if (graphiteHost != null) { log.debug("Initializing Metrics Graphite reporting"); Integer graphitePort = env.getProperty("tatami.metrics.graphite.port", Integer.class); GraphiteReporter.enable(1, TimeUnit.MINUTES, graphiteHost, graphitePort); } else { log.warn("Graphite server is not configured, unable to send any data to Graphite"); } } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/SearchConfiguration.java ================================================ package fr.ippon.tatami.config; import fr.ippon.tatami.service.elasticsearch.ElasticsearchEngine; import fr.ippon.tatami.service.elasticsearch.EmbeddedElasticsearchEngine; import fr.ippon.tatami.service.elasticsearch.RemoteElasticsearchEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import javax.inject.Inject; /** * Search configuration, with Elastic Search. */ @Configuration public class SearchConfiguration { private final Logger log = LoggerFactory.getLogger(SearchConfiguration.class); @Inject private Environment env; @Bean public ElasticsearchEngine elasticsearchEngine() { log.info("Starting Elasticsearch"); String mode = env.getRequiredProperty("elasticsearch.engine.mode"); if (Constants.REMOTE_ENGINE.equalsIgnoreCase(mode)) { return new RemoteElasticsearchEngine(); } else if (Constants.EMBEDDED_ENGINE.equalsIgnoreCase(mode)) { return new EmbeddedElasticsearchEngine(); } else { //Log do not support log.fatal log.error("Elasticsearch engine mode is not defined, please configure the \"elasticsearch.engine.mode\" property"); throw new IllegalArgumentException("Elasticsearch engine mode " + mode + " not defined"); } } @Bean public String indexNamePrefix() { return env.getProperty("elasticsearch.indexNamePrefix"); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/metrics/CassandraHealthCheck.java ================================================ package fr.ippon.tatami.config.metrics; import com.yammer.metrics.core.HealthCheck; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.exceptions.HectorException; import static fr.ippon.tatami.config.ColumnFamilyKeys.DOMAIN_CF; import static me.prettyprint.hector.api.factory.HFactory.createRangeSlicesQuery; /** * Metrics HealthCheck for Cassandra. */ public class CassandraHealthCheck extends HealthCheck { private final Keyspace keyspaceOperator; public CassandraHealthCheck(Keyspace keyspaceOperator) { super("Cassandra"); this.keyspaceOperator = keyspaceOperator; } @Override public Result check() throws Exception { try { createRangeSlicesQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(DOMAIN_CF) .setRange(null, null, false, 1) .execute() .get(); return Result.healthy(); } catch (HectorException he) { return Result.unhealthy("Cannot connect to Cassandra Cluster : " + keyspaceOperator.getKeyspaceName()); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/config/metrics/JavaMailHealthCheck.java ================================================ package fr.ippon.tatami.config.metrics; import com.yammer.metrics.core.HealthCheck; import fr.ippon.tatami.service.MailService; /** * Metrics HealthCheck for JavaMail. */ public class JavaMailHealthCheck extends HealthCheck { private final MailService mailService; public JavaMailHealthCheck(MailService mailService) { super("JavaMail"); this.mailService = mailService; } @Override public Result check() throws Exception { if (mailService.connectSmtpServer()) { return Result.healthy(); } else { return Result.unhealthy("Cannot connect to Mail server"); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/Attachment.java ================================================ package fr.ippon.tatami.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.DateTimeFormatterBuilder; import java.io.Serializable; import java.util.Date; public class Attachment implements Serializable { private static final DateTimeFormatter oldDateFormatter = new DateTimeFormatterBuilder() .appendDayOfMonth(1) .appendLiteral(' ') .appendMonthOfYearShortText() .appendLiteral(' ') .appendYear(4, 4) .toFormatter(); private String attachmentId; private String filename; @JsonIgnore private Date creationDate; private String prettyPrintCreationDate; @JsonIgnore private byte[] content; @JsonIgnore private byte[] thumbnail; private boolean hasThumbnail = false; private long size; public String getAttachmentId() { return attachmentId; } public void setAttachmentId(String attachmentId) { this.attachmentId = attachmentId; } public String getFilename() { return filename; } public void setFilename(String filename) { this.filename = filename; } public Date getCreationDate() { return creationDate; } public void setCreationDate(Date creationDate) { this.creationDate = creationDate; DateTime dateTime = new DateTime(creationDate); this.prettyPrintCreationDate = oldDateFormatter.print(dateTime); } public String getPrettyPrintCreationDate() { return prettyPrintCreationDate; } public void setPrettyPrintCreationDate(String prettyPrintCreationDate) { this.prettyPrintCreationDate = prettyPrintCreationDate; } public byte[] getContent() { return content; } public void setContent(byte[] content) { this.content = content; } public byte[] getThumbnail() { return thumbnail; } public void setThumbnail(byte[] thumbnail) { this.thumbnail = thumbnail; } public boolean getHasThumbnail() { return this.hasThumbnail; } public void setHasThumbnail(boolean hasThumbnail) { this.hasThumbnail = hasThumbnail; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Attachment that = (Attachment) o; return attachmentId.equals(that.attachmentId); } @Override public int hashCode() { return attachmentId.hashCode(); } @Override public String toString() { return "Attachment{" + "attachmentId='" + attachmentId + '\'' + ", filename='" + filename + '\'' + ", prettyPrintCreationDate='" + prettyPrintCreationDate + '\'' + ", size=" + size + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/Avatar.java ================================================ package fr.ippon.tatami.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import java.io.Serializable; import java.util.Date; public class Avatar implements Serializable { private String avatarId; private String filename; @JsonIgnore private Date creationDate; @JsonIgnore private byte[] content; private long size; public String getAvatarId() { return avatarId; } public void setAvatarId(String AvatarId) { this.avatarId = AvatarId; } public String getFilename() { return filename; } public void setFilename(String filename) { this.filename = filename; } public Date getCreationDate() { return creationDate; } public void setCreationDate(Date creationDate) { this.creationDate = creationDate; } public byte[] getContent() { return content; } public void setContent(byte[] content) { this.content = content; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } @Override public int hashCode() { return avatarId.hashCode(); } @Override public String toString() { return "Avatar{" + "avatarId='" + avatarId + '\'' + ", filename='" + filename + '\'' + ", CreationDate='" + creationDate + '\'' + ", size=" + size + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/DigestType.java ================================================ package fr.ippon.tatami.domain; /** * @author Pierre Rust */ public enum DigestType { WEEKLY_DIGEST("WEEKLY"), DAILY_DIGEST("DAILY"); private DigestType(final String text) { this.text = text; } private final String text; /* (non-Javadoc) * @see java.lang.Enum#toString() */ @Override public String toString() { return text; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/Domain.java ================================================ package fr.ippon.tatami.domain; import java.io.Serializable; /** * A domain is a domain name (e.g. "ippon.fr"), and represents a company. */ public class Domain implements Serializable, Comparable { private String name; private int numberOfUsers; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getNumberOfUsers() { return numberOfUsers; } public void setNumberOfUsers(int numberOfUsers) { this.numberOfUsers = numberOfUsers; } @Override public int compareTo(Domain other) { return this.name.compareTo(other.name); } @Override public boolean equals(Object o) { if (this == o) { return true; } else if (o == null || getClass() != o.getClass()) { return false; } Domain domain = (Domain) o; return name.equals(domain.name); } @Override public int hashCode() { return name.hashCode(); } @Override public String toString() { return "Domain{" + "name='" + name + '\'' + ", numberOfUsers=" + numberOfUsers + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/DomainConfiguration.java ================================================ package fr.ippon.tatami.domain; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.Properties; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import org.apache.commons.io.IOUtils; /** * The configuration for a specific domain. * * @author Julien Dubois */ @Entity @Table(name = "DomainConfiguration") public class DomainConfiguration implements Serializable { public static class SubscriptionAndStorageSizeOptions { public static String BASICSIZE = "10"; public static String PREMIUMSIZE = "1000"; public static String IPPONSIZE = "100000"; public static String BASICSUSCRIPTION = "0"; public static String PREMIUMSUSCRIPTION = "1"; public static String IPPONSUSCRIPTION = "-1"; static{ InputStream inputStream=null; try{ inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("META-INF/tatami/tatami.properties"); Properties props = new Properties(); props.load(inputStream); String basicSize = "storage.basic.max.size"; String premiumSize = "storage.premium.max.size"; String ipponSize = "storage.ippon.max.size"; String basicSuscription = "suscription.level.free"; String premiumSuscription = "suscription.level.premium"; String ipponSuscription = "suscription.level.ippon"; if(!props.containsKey(basicSize) || !props.containsKey(premiumSize) || !props.containsKey(ipponSize) || !props.containsKey(basicSuscription) || !props.containsKey(premiumSuscription) || !props.containsKey(ipponSuscription)) throw new IllegalStateException("Property not found"); BASICSIZE= props.getProperty(basicSize); PREMIUMSIZE= props.getProperty(premiumSize); IPPONSIZE= props.getProperty(ipponSize); BASICSUSCRIPTION= props.getProperty(basicSuscription); PREMIUMSUSCRIPTION= props.getProperty(premiumSuscription); IPPONSUSCRIPTION= props.getProperty(ipponSuscription); } catch(IOException e){ throw new IllegalStateException(e); }finally{ // apache commons / IO IOUtils.closeQuietly(inputStream); } } } @Id private String domain; @Column(name = "subscriptionLevel") private String subscriptionLevel; @Column(name = "storageSize") private String storageSize; @Column(name = "adminLogin") private String adminLogin; public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public String getSubscriptionLevel() { return subscriptionLevel; } public void setSubscriptionLevel(String subscriptionLevel) { this.subscriptionLevel = subscriptionLevel; } public String getStorageSize() { return storageSize; } public long getStorageSizeAsLong() { try { return Long.parseLong(this.storageSize) * 1000000; } catch (NumberFormatException nfe) { return Long.parseLong(SubscriptionAndStorageSizeOptions.BASICSIZE) * 1000000; } } public void setStorageSize(String storageSize) { this.storageSize = storageSize; } public String getAdminLogin() { return adminLogin; } public void setAdminLogin(String adminLogin) { this.adminLogin = adminLogin; } @Override public String toString() { return "DomainConfiguration{" + "domain='" + domain + '\'' + ", subscriptionLevel='" + subscriptionLevel + '\'' + ", storageSize='" + storageSize + '\'' + ", adminLogin='" + adminLogin + '\'' + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/Group.java ================================================ package fr.ippon.tatami.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import java.io.Serializable; /** * A group. */ public class Group implements Comparable, Serializable, Cloneable { private String groupId; private boolean publicGroup; private boolean archivedGroup; private String name; private String description; @JsonIgnore private String domain; private long counter; private boolean member; private boolean administrator; public String getGroupId() { return groupId; } public void setGroupId(String groupId) { this.groupId = groupId; } public boolean isPublicGroup() { return publicGroup; } public void setPublicGroup(boolean publicGroup) { this.publicGroup = publicGroup; } public boolean isArchivedGroup() { return archivedGroup; } public void setArchivedGroup(boolean archivedGroup) { this.archivedGroup = archivedGroup; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public long getCounter() { return counter; } public void setCounter(long counter) { this.counter = counter; } public boolean isMember() { return member; } public void setMember(boolean member) { this.member = member; } public boolean isAdministrator() { return administrator; } public void setAdministrator(boolean administrator) { this.administrator = administrator; } @Override public int compareTo(Group o) { if (this.getName() == null) { return -1; } if (o.getName() == null) { return 1; } if (this.getName().equals(o.getName())) { return this.getGroupId().compareTo(o.getGroupId()); // To display duplicates } return this.getName().compareTo(o.getName()); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Group group = (Group) o; return groupId.equals(group.groupId); } @Override public int hashCode() { return groupId.hashCode(); } @Override public String toString() { return "Group{" + "groupId='" + groupId + '\'' + ", publicGroup=" + publicGroup + ", archivedGroup=" + archivedGroup + ", name='" + name + '\'' + ", domain='" + domain + '\'' + ", counter=" + counter + ", member=" + member + ", administrator=" + administrator + '}'; } @Override public Object clone() { Group clone = null; try { clone = (Group) super.clone(); } catch (CloneNotSupportedException cnse) { cnse.printStackTrace(System.err); } return clone; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/User.java ================================================ package fr.ippon.tatami.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import fr.ippon.tatami.domain.validation.ContraintsUserCreation; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import javax.validation.groups.Default; import java.io.Serializable; /** * A user. * * @author Julien Dubois */ @Entity @Table(name = "User") public class User implements Serializable { @NotEmpty(message = "Login is mandatory.", groups = {ContraintsUserCreation.class, Default.class}) @NotNull(message = "Login is mandatory.", groups = {ContraintsUserCreation.class, Default.class}) @Email(message = "Email is invalid.") @Id @JsonIgnore private String login; @Column(name = "password") @JsonIgnore private String password; @Column(name = "username") private String username; @Column(name = "domain") @JsonIgnore private String domain; @Column(name = "avatar") private String avatar; @Size(min = 0, max = 50) @Column(name = "firstName") private String firstName; @Size(min = 0, max = 50) @Column(name = "lastName") private String lastName; @Size(min = 0, max = 100) @Column(name = "jobTitle") private String jobTitle; @Size(min = 0, max = 20) @Column(name = "phoneNumber") private String phoneNumber; @Column(name = "openIdUrl") @JsonIgnore private String openIdUrl; @Column(name = "preferences_mention_email") @JsonIgnore private Boolean preferencesMentionEmail; @Column(name = "rssUid") @JsonIgnore private String rssUid; @Column(name = "weekly_digest_subscription") @JsonIgnore private Boolean weeklyDigestSubscription; @Column(name = "daily_digest_subscription") @JsonIgnore private Boolean dailyDigestSubscription; @Column(name = "attachmentsSize") private long attachmentsSize; @Column(name="activated") private Boolean activated=true; private long statusCount; private long friendsCount; private long followersCount; private Boolean isAdmin = false; public Boolean getActivated() { return activated; } public void setActivated(Boolean activated) { this.activated = activated; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getJobTitle() { return jobTitle; } public void setJobTitle(String jobTitle) { this.jobTitle = jobTitle; } public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } public String getOpenIdUrl() { return openIdUrl; } public void setOpenIdUrl(String openIdUrl) { this.openIdUrl = openIdUrl; } public Boolean getPreferencesMentionEmail() { return preferencesMentionEmail; } public void setPreferencesMentionEmail(Boolean preferencesMentionEmail) { this.preferencesMentionEmail = preferencesMentionEmail; } public long getAttachmentsSize() { return attachmentsSize; } public void setAttachmentsSize(long attachmentsSize) { this.attachmentsSize = attachmentsSize < 0 ? 0 : attachmentsSize; } public String getRssUid() { return rssUid; } public void setRssUid(String rssUid) { this.rssUid = rssUid; } public long getStatusCount() { return statusCount; } public void setStatusCount(long statusCount) { this.statusCount = statusCount; } public long getFriendsCount() { return friendsCount; } public void setFriendsCount(long friendsCount) { this.friendsCount = friendsCount; } public long getFollowersCount() { return followersCount; } public void setFollowersCount(long followersCount) { this.followersCount = followersCount; } public Boolean getIsAdmin() { return isAdmin; } public void setIsAdmin(boolean isAdmin) { this.isAdmin = isAdmin; } public Boolean getWeeklyDigestSubscription() { return weeklyDigestSubscription; } public void setWeeklyDigestSubscription(Boolean weeklyDigestSubscription) { this.weeklyDigestSubscription = weeklyDigestSubscription; } public Boolean getDailyDigestSubscription() { return dailyDigestSubscription; } public void setDailyDigestSubscription(Boolean dailyDigestSubscription) { this.dailyDigestSubscription = dailyDigestSubscription; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return login.equals(user.login); } @Override public int hashCode() { return login.hashCode(); } @Override public String toString() { return "User{" + "login='" + login + '\'' + ", password='" + password + '\'' + ", username='" + username + '\'' + ", domain='" + domain + '\'' + ", avatar='" + avatar + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", jobTitle='" + jobTitle + '\'' + ", phoneNumber='" + phoneNumber + '\'' + ", openIdUrl='" + openIdUrl + '\'' + ", preferencesMentionEmail=" + preferencesMentionEmail + ", rssUid=" + rssUid + ", dailyDigestSubscription=" + dailyDigestSubscription + ", weeklyDigestSubscription=" + weeklyDigestSubscription + ", attachmentsSize=" + attachmentsSize + ", activated=" + activated + ", statusCount=" + statusCount + ", friendsCount=" + friendsCount + ", followersCount=" + followersCount + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/UserStatusStat.java ================================================ package fr.ippon.tatami.domain; import java.io.Serializable; public class UserStatusStat implements Comparable, Serializable { private String username; private Long statusCount; public UserStatusStat(String username, Long count) { assert username != null && count != null; this.username = username; this.statusCount = count; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Long getStatusCount() { return statusCount; } public void setStatusCount(Long count) { this.statusCount = count; } @Override public int compareTo(UserStatusStat o) { return this.username.compareTo(o.username); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserStatusStat that = (UserStatusStat) o; return username.equals(that.username); } @Override public int hashCode() { return username.hashCode(); } @Override public String toString() { return "UserStatusStat{" + "username='" + username + '\'' + ", statusCount=" + statusCount + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/status/AbstractStatus.java ================================================ package fr.ippon.tatami.domain.status; import javax.validation.constraints.NotNull; import java.io.Serializable; import java.util.Date; /** * Parent class for all statuses. */ public abstract class AbstractStatus implements Serializable { private String statusId; @NotNull private StatusType type; @NotNull private String login; @NotNull private String username; @NotNull private String domain; private Date statusDate; public String getGeoLocalization() { return geoLocalization; } public void setGeoLocalization(String geoLocalization) { this.geoLocalization = geoLocalization; } private String geoLocalization; private boolean removed; public String getStatusId() { return statusId; } public void setStatusId(String statusId) { this.statusId = statusId; } public StatusType getType() { return type; } public void setType(StatusType type) { this.type = type; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public Date getStatusDate() { return statusDate; } public void setStatusDate(Date statusDate) { this.statusDate = statusDate; } public boolean isRemoved() { return removed; } public void setRemoved(boolean removed) { this.removed = removed; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AbstractStatus that = (AbstractStatus) o; if (statusId != null ? !statusId.equals(that.statusId) : that.statusId != null) return false; return true; } @Override public int hashCode() { return statusId != null ? statusId.hashCode() : 0; } @Override public String toString() { return "AbstractStatus{" + "statusId='" + statusId + '\'' + ", type=" + type + ", login='" + login + '\'' + ", username='" + username + '\'' + ", domain='" + domain + '\'' + ", statusDate=" + statusDate + ", geoLocalization='" + geoLocalization + '\'' + ", removed=" + removed + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/status/Announcement.java ================================================ package fr.ippon.tatami.domain.status; /** * An announcement. */ public class Announcement extends Share { } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/status/MentionFriend.java ================================================ package fr.ippon.tatami.domain.status; /** * Mention a user that someone started following him. */ public class MentionFriend extends AbstractStatus { private String followerLogin; public String getFollowerLogin() { return followerLogin; } public void setFollowerLogin(String followerLogin) { this.followerLogin = followerLogin; } @Override public String toString() { return "MentionFriend{" + "followerLogin='" + followerLogin + '\'' + "} " + super.toString(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/status/MentionShare.java ================================================ package fr.ippon.tatami.domain.status; /** * Mention a user that one of his statuses was shared. */ public class MentionShare extends Share { } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/status/Share.java ================================================ package fr.ippon.tatami.domain.status; /** * A status that is shared. */ public class Share extends AbstractStatus { private String originalStatusId; public String getOriginalStatusId() { return originalStatusId; } public void setOriginalStatusId(String originalStatusId) { this.originalStatusId = originalStatusId; } @Override public String toString() { return "Share{" + "originalStatusId='" + originalStatusId + '\'' + "} " + super.toString(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/status/Status.java ================================================ package fr.ippon.tatami.domain.status; import fr.ippon.tatami.domain.Attachment; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.util.Collection; /** * A status. * * @author Julien Dubois */ public class Status extends AbstractStatus { private String groupId; private Boolean statusPrivate; private Boolean hasAttachments; private Collection attachments; @NotNull @NotEmpty(message = "Content field is mandatory.") @Size(min = 1, max = 2048) private String content; /** * If this status is a reply, the statusId of the original status. */ private String discussionId; /** * If this status is a reply, the statusId of the status that is being replied to. */ private String replyTo; /** * If this status is a reply, the username of the status that is being replied to. */ private String replyToUsername; private boolean detailsAvailable; private Boolean removed; public String getGroupId() { return groupId; } public void setGroupId(String groupId) { this.groupId = groupId; } public Boolean getStatusPrivate() { return statusPrivate; } public void setStatusPrivate(Boolean statusPrivate) { this.statusPrivate = statusPrivate; } public Boolean getHasAttachments() { return hasAttachments; } public void setHasAttachments(Boolean hasAttachments) { this.hasAttachments = hasAttachments; } public Collection getAttachments() { return attachments; } public void setAttachments(Collection attachments) { this.attachments = attachments; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getDiscussionId() { return discussionId; } public void setDiscussionId(String discussionId) { this.discussionId = discussionId; } public String getReplyTo() { return replyTo; } public void setReplyTo(String replyTo) { this.replyTo = replyTo; } public String getReplyToUsername() { return replyToUsername; } public void setReplyToUsername(String replyToUsername) { this.replyToUsername = replyToUsername; } public boolean isDetailsAvailable() { return detailsAvailable; } public void setDetailsAvailable(boolean detailsAvailable) { this.detailsAvailable = detailsAvailable; } public Boolean getRemoved() { return removed; } public void setRemoved(Boolean removed) { this.removed = removed; } @Override public String toString() { return "Status{" + "groupId='" + groupId + '\'' + ", statusPrivate=" + statusPrivate + ", hasAttachments=" + hasAttachments + ", attachments=" + attachments + ", content='" + content + '\'' + ", discussionId='" + discussionId + '\'' + ", replyTo='" + replyTo + '\'' + ", replyToUsername='" + replyToUsername + '\'' + ", detailsAvailable=" + detailsAvailable + ", removed=" + removed + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/status/StatusDetails.java ================================================ package fr.ippon.tatami.domain.status; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.service.dto.StatusDTO; import java.io.Serializable; import java.util.Collection; /** * Extended information for a status. * - Lists the discussion related to this status * - Lists the users who shared this status */ public class StatusDetails implements Serializable { private String statusId; private Collection discussionStatuses; private Collection sharedByLogins; public String getStatusId() { return statusId; } public void setStatusId(String statusId) { this.statusId = statusId; } public Collection getDiscussionStatuses() { return discussionStatuses; } public void setDiscussionStatuses(Collection discussionStatuses) { this.discussionStatuses = discussionStatuses; } public Collection getSharedByLogins() { return sharedByLogins; } public void setSharedByLogins(Collection sharedByLogins) { this.sharedByLogins = sharedByLogins; } @Override public String toString() { return "StatusDetails{" + "StatusId='" + statusId + '\'' + ", discussionStatuses=" + discussionStatuses + ", sharedByLogins=" + sharedByLogins + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/status/StatusType.java ================================================ package fr.ippon.tatami.domain.status; public enum StatusType { STATUS, SHARE, ANNOUNCEMENT, MENTION_FRIEND, MENTION_SHARE } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/validation/ContraintsAttachmentCreation.java ================================================ package fr.ippon.tatami.domain.validation; public interface ContraintsAttachmentCreation { } ================================================ FILE: services/src/main/java/fr/ippon/tatami/domain/validation/ContraintsUserCreation.java ================================================ package fr.ippon.tatami.domain.validation; /** * Bean Validation group used to validate the creation of a user. * * @author Julien Dubois */ public interface ContraintsUserCreation { } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/AppleDeviceRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; public interface AppleDeviceRepository { void createAppleDevice(String login, String deviceId); void removeAppleDevice(String login, String deviceId); Collection findAppleDevices(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/AppleDeviceUserRepository.java ================================================ package fr.ippon.tatami.repository; public interface AppleDeviceUserRepository { void createAppleDeviceForUser(String deviceId, String login); void removeAppleDeviceForUser(String deviceId); String findLoginForDeviceId(String deviceId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/AttachmentRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.Attachment; public interface AttachmentRepository { void createAttachment(Attachment attach); void deleteAttachment(Attachment attach); /** * Finds an attachment, including its content (the file data). */ Attachment findAttachmentById(String attachmentId); /** * Only fetch the attachment metadata : file name & size, but not its content. */ Attachment findAttachmentMetadataById(String attachmentId); /** * Update the thumbnail of the given attachment */ Attachment updateThumbnail(Attachment attach); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/AvatarRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.Avatar; public interface AvatarRepository { void createAvatar(Avatar avatar); void removeAvatar(String avatarId); Avatar findAvatarById(String avatarId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/BlockRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.User; import java.util.Collection; /** *This repository stores the the relationships blockingUser-blockedUser. * Similar strucure as the FriendRepository. */ public interface BlockRepository { void blockUser(String currentUserLogin, String blockedUserLogin); void unblockUser(String currentUserLogin, String unblockedUserLogin); Collection getUsersBlockedBy(String userLogin); boolean isBlocked(String blockingLogin, String blockedLogin); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/CounterRepository.java ================================================ package fr.ippon.tatami.repository; /** * The Counter Repository. * * @author Julien Dubois */ public interface CounterRepository { void incrementFollowersCounter(String login); void incrementFriendsCounter(String login); void incrementStatusCounter(String login); void decrementFollowersCounter(String login); void decrementFriendsCounter(String login); void decrementStatusCounter(String login); long getFollowersCounter(String login); long getFriendsCounter(String login); long getStatusCounter(String login); void createFollowersCounter(String login); void createFriendsCounter(String login); void createStatusCounter(String login); void deleteCounters(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/DaylineRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.UserStatusStat; import fr.ippon.tatami.domain.status.Status; import java.util.Collection; /** * The Dayline Repository, which stores statistics per day. * * @author Julien Dubois */ public interface DaylineRepository { /** * Add a status to the repository. */ void addStatusToDayline(Status status, String day); /** * Get the statistics for one day, in the form <username, number of status updates>. */ Collection getDayline(String domain, String day); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/DiscussionRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; /** * The StatusDetails Repository. * * @author Julien Dubois */ public interface DiscussionRepository { void addReplyToDiscussion(String originalStatusId, String replyStatusId); Collection findStatusIdsInDiscussion(String originalStatusId); boolean hasReply(String statusId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/DomainConfigurationRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.DomainConfiguration; /** * The DomainConfiguraiton Repository. * * @author Julien Dubois */ public interface DomainConfigurationRepository { void updateDomainConfiguration(DomainConfiguration domainConfiguration); DomainConfiguration findDomainConfigurationByDomain(String domain); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/DomainRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.Domain; import java.util.List; import java.util.Set; /** * The Domain Repository. * * @author Julien Dubois */ public interface DomainRepository { void addUserInDomain(String domain, String login); void updateUserInDomain(String domain, String login); void deleteUserInDomain(String domain, String login); List getLoginsInDomain(String domain, int pagination); List getLoginsInDomain(String domain); Set getAllDomains(); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/DomainlineRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; import java.util.List; /** * The Domainline Repository. * * @author Julien Dubois */ public interface DomainlineRepository { /** * Add a status to the Domain line. */ void addStatusToDomainline(String domain, String statusId); /** * Remove a collection of statuses from the Domain line. */ void removeStatusFromDomainline(String domain, Collection statusIdsToDelete); /** * The Domainline : the public status for a domain. * - The name is the statusId of the statuses * - Value is always null */ List getDomainline(String domain, int size, String start, String finish); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/FavoritelineRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.List; /** * The Favoriteline Repository. * * @author Julien Dubois */ public interface FavoritelineRepository { void addStatusToFavoriteline(String login, String statusId); void removeStatusFromFavoriteline(String login, String statusId); void deleteFavoriteline(String login); /** * The favoriteline : the statuses fovorited by the user. * - The key is the statusId of the statuses * - The value is always null */ List getFavoriteline(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/FollowerRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; /** * The Follower Repository. * * @author Julien Dubois */ public interface FollowerRepository { void addFollower(String login, String followerLogin); void removeFollower(String login, String followerLogin); Collection findFollowersForUser(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/FriendRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.List; /** * The Friend Repository. * * @author Julien Dubois */ public interface FriendRepository { void addFriend(String login, String friendLogin); void removeFriend(String login, String friendLogin); List findFriendsForUser(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/GroupCounterRepository.java ================================================ package fr.ippon.tatami.repository; /** * The Group Counter Repository. * * @author Julien Dubois */ public interface GroupCounterRepository { long getGroupCounter(String domain, String groupId); void incrementGroupCounter(String domain, String groupId); void decrementGroupCounter(String domain, String groupId); void deleteGroupCounter(String domain, String groupId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/GroupDetailsRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.Group; /** * The Group Details Repository. * * @author Julien Dubois */ public interface GroupDetailsRepository { void createGroupDetails(String groupId, String name, String description, boolean publicGroup); Group getGroupDetails(String groupId); void editGroupDetails(String groupId, String name, String description, boolean archivedGroup); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/GroupMembersRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Map; /** * The Group members Repository. * * @author Julien Dubois */ public interface GroupMembersRepository { void addMember(String groupId, String login); void addAdmin(String groupId, String login); void removeMember(String groupId, String login); Map findMembers(String groupId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/GroupRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.Group; /** * The Group Repository. * * @author Julien Dubois */ public interface GroupRepository { String createGroup(String domain); Group getGroupById(String domain, String groupId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/GrouplineRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; import java.util.List; /** * The Groupline Repository. * * @author Julien Dubois */ public interface GrouplineRepository { /** * Add a status to the Group line. */ void addStatusToGroupline(String groupId, String statusId); /** * Remove a collection of statuses from the Group line. */ void removeStatusesFromGroupline(String groupId, Collection statusIdsToDelete); /** * The Groupline : the statuses for a given group. * - The name is the statusId of the statuses * - Value is always null */ List getGroupline(String groupId, int size, String start, String finish); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/IdempotentRepository.java ================================================ package fr.ippon.tatami.repository; /** * Used to de-deplucate Camel messages. */ public interface IdempotentRepository extends org.apache.camel.spi.IdempotentRepository { @Override boolean add(String key); @Override boolean contains(String key); @Override boolean remove(String key); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/MailDigestRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.DigestType; import java.util.List; /** * Provide access to digest subscription info. *

    * Subscription are organized by digest type and domain (in order to allow * domain-level configuration). * * @author Pierre Rust */ public interface MailDigestRepository { /** * Subscribe an user to email digest. */ void subscribeToDigest(DigestType digestType, String login, String domain, String day); /** * Un-subscribe an user from a domain. */ void unsubscribeFromDigest(DigestType digestType, String login, String domain, String day); /** * Retrieves the list of logins in a domain subscribed to a given digest type. */ List getLoginsRegisteredToDigest(DigestType digestType, String domain, String day, int pagination); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/MentionlineRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; import java.util.List; /** * The Mentionline Repository. * * @author Julien Dubois */ public interface MentionlineRepository { /** * Add a status to the Mention line. */ void addStatusToMentionline(String mentionedLogin, String statusId); /** * Remove a collection of statuses from the Mention line. */ void removeStatusesFromMentionline(String mentionedLogin, Collection statusIdsToDelete); /** * The mention line : the mentions for a given user. * - The name is the statusId of the statuses * - Value is always null */ List getMentionline(String login, int size, String start, String finish); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/RegistrationRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Map; /** * The Registration Repository. * * @author Julien Dubois */ public interface RegistrationRepository { String generateRegistrationKey(String login); String getLoginByRegistrationKey(String registrationKey); /** * !! For testing purpose only !! */ Map _getAllRegistrationKeyByLogin(); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/ResolvedReportRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; /** * Created by emilyklein on 7/13/16. */ public interface ResolvedReportRepository { void resolvedReport (String reportingUser, String reportedStatusId, Long timeReported, String adminResolved, String actionTaken); Collection findResolvedReportsById(String statusId); boolean hasBeenResolved(String statusId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/RssUidRepository.java ================================================ package fr.ippon.tatami.repository; /** * The Rss Uid Repository. *

    * The RSS uid is used to build an anonymous url to a RSS channel * representing an user timeline. * * @author Pierre Rust */ public interface RssUidRepository { String generateRssUid(String login); void removeRssUid(String rssUid); String getLoginByRssUid(String rssUid); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/SharesRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; /** * The StatusDetails Repository. * * @author Julien Dubois */ public interface SharesRepository { void newShareByLogin(String statusId, String sharedByLogin); Collection findLoginsWhoSharedAStatus(String statusId); boolean hasBeenShared(String statusId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/StatusAttachmentRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; /** * Stores attachment IDs for a status. */ public interface StatusAttachmentRepository { void addAttachmentId(String statusId, String attachmentId); void removeAttachmentId(String statusId, String attachmentId); Collection findAttachmentIds(String statusId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/StatusReportRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; import java.util.List; public interface StatusReportRepository { void reportStatus (String domain, String reportedStatusId, String reportingLogin); void unreportStatus (String domain, String reportedStatusId); List findReportedStatuses(String domain); String findUserHavingReported(String domain, String statusId); boolean hasBeenReportedByUser(String domain, String reportedStatusId, String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/StatusRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.status.*; import javax.validation.ConstraintViolationException; import java.util.Collection; /** * The Status Repository. * * @author Julien Dubois */ public interface StatusRepository { Status createStatus(String login, boolean statusPrivate, Group group, Collection attachmentIds, String content, String discussionId, String replyTo, String replyToUsername, String geoLocalization) throws ConstraintViolationException; Share createShare(String login, String originalStatusId); Announcement createAnnouncement(String login, String originalStatusId); MentionFriend createMentionFriend(String login, String followerLogin); MentionShare createMentionShare(String login, String originalStatusId); void removeStatus(AbstractStatus status); /** * Retrieve a persisted status. * * @return null if status was removed */ AbstractStatus findStatusById(String statusId); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/TagCounterRepository.java ================================================ package fr.ippon.tatami.repository; /** * The Tag Counter Repository. * * @author Julien Dubois */ public interface TagCounterRepository { long getTagCounter(String domain, String tag); void incrementTagCounter(String domain, String tag); void decrementTagCounter(String domain, String tag); void deleteTagCounter(String domain, String tag); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/TagFollowerRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; /** * Specific Follower repository for tags. */ public interface TagFollowerRepository { void addFollower(String domain, String tag, String login); void removeFollower(String domain, String tag, String login); Collection findFollowers(String domain, String tag); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/TaglineRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.status.Status; import java.util.Collection; import java.util.List; /** * The Tagline Repository. * * @author Julien Dubois */ public interface TaglineRepository { /** * Add a status to the Tag line. */ void addStatusToTagline(String tag, Status status); /** * Remove a collection of statuses from the Tag line. */ void removeStatusesFromTagline(String tag, String domain, Collection statusIdsToDelete); /** * The tagline : the statuses for a given tag. * - The name is the statusId of the statuses * - Value is always null */ List getTagline(String domain, String tag, int size, String start, String finish); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/TimelineRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.status.Announcement; import fr.ippon.tatami.domain.status.Share; import java.util.Collection; import java.util.List; /** * The Timeline Repository. *

    * A Timeline is the list of statuses that a user sees, which includes : * - The statuses from the people he follows * - His own statuses * - Statuses that were shared by the people he follows or by himself * * @author Julien Dubois */ public interface TimelineRepository { boolean isStatusInTimeline(String login, String statusId); void addStatusToTimeline(String login, String statusId); void removeStatusesFromTimeline(String login, Collection statusIdsToDelete); void shareStatusToTimeline(String sharedByLogin, String timelineLogin, Share share); void announceStatusToTimeline(String announcedByLogin, List logins, Announcement announcement); void deleteTimeline(String login); /** * The user timeline : the user's statuses, and statuses from users he follows. * - The key is the statusId of the statuses * - The value is always null */ List getTimeline(String login, int size, String start, String finish); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/TrendRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; import java.util.List; /** * The Trends repository : stores and retrieves tags trends. */ public interface TrendRepository { void addTag(String domain, String tag); List getRecentTags(String domain); List getRecentTags(String domain, int maxNumber); Collection getDomainTags(String domain); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/UserAttachmentRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; /** * Stores attachment IDs for a user. */ public interface UserAttachmentRepository { void addAttachmentId(String login, String attachmentId); void removeAttachmentId(String login, String attachmentId); Collection findAttachmentIds(String login, int pagination, String finish); Collection findAttachmentIds(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/UserGroupRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; import java.util.List; /** * The User group Repository. * * @author Julien Dubois */ public interface UserGroupRepository { void addGroupAsMember(String login, String groupId); void addGroupAsAdmin(String login, String groupId); void removeGroup(String login, String groupId); List findGroups(String login); Collection findGroupsAsAdmin(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/UserRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.User; import javax.validation.ConstraintViolationException; /** * The User Repository. * * @author Julien Dubois */ public interface UserRepository { void createUser(User user); void updateUser(User user) throws ConstraintViolationException, IllegalArgumentException; void deleteUser(User user); void desactivateUser( User user ); void reactivateUser( User user ); User findUserByLogin(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/UserTagRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; /** * Specific Friend repository for tags. */ public interface UserTagRepository { void addTag(String login, String tag); void removeTag(String login, String tag); Collection findTags(String login); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/UserTrendRepository.java ================================================ package fr.ippon.tatami.repository; import java.util.Collection; import java.util.Date; import java.util.List; /** * The User Trends repository : stores user trends. */ public interface UserTrendRepository { void addTag(String login, String tag); List getRecentTags(String login); Collection getUserRecentTags(String login, Date endDate, int nbRecentTags); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/UserlineRepository.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.domain.status.Share; import java.util.Collection; import java.util.List; /** * The Userline Repository. *

    * A Userline is the list of statuses updated by a user (including the statuses he shared). * * @author Julien Dubois */ public interface UserlineRepository { /** * Add a status to the user line. */ void addStatusToUserline(String login, String statusId); /** * Remove a collection of statuses from the user line. */ void removeStatusesFromUserline(String login, Collection statusIdsToDelete); void shareStatusToUserline(String currentLogin, Share share); void deleteUserline(String login); /** * The userline : the user's statuses. * - The key is the statusId of the statuses * - The value is always null */ List getUserline(String login, int size, String start, String finish); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/AbstractCassandraFollowerRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.Constants; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.service.template.ColumnFamilyResult; import me.prettyprint.cassandra.service.template.ColumnFamilyTemplate; import me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; /** * Abstract class for managing followers : users who follow another user or a tag. */ public abstract class AbstractCassandraFollowerRepository { private ColumnFamilyTemplate template; @Inject private Keyspace keyspaceOperator; @PostConstruct public void init() { template = new ThriftColumnFamilyTemplate(keyspaceOperator, getFollowersCF(), StringSerializer.get(), StringSerializer.get()); template.setCount(Constants.CASSANDRA_MAX_COLUMNS); } void addFollower(String key, String followerKey) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(key, getFollowersCF(), HFactory.createColumn(followerKey, Calendar.getInstance().getTimeInMillis(), StringSerializer.get(), LongSerializer.get())); } void removeFollower(String key, String followerKey) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(key, getFollowersCF(), followerKey, StringSerializer.get()); } Collection findFollowers(String key) { ColumnFamilyResult result = template.queryColumns(key); Collection followers = new ArrayList(); for (String columnName : result.getColumnNames()) { followers.add(columnName); } return followers; } protected abstract String getFollowersCF(); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/AbstractCassandraFriendRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.Constants; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.service.template.ColumnFamilyResult; import me.prettyprint.cassandra.service.template.ColumnFamilyTemplate; import me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.ArrayList; import java.util.Calendar; import java.util.List; /** * Abstract class for managing friends : users or tags that a user follows. */ public abstract class AbstractCassandraFriendRepository { private ColumnFamilyTemplate friendsTemplate; @Inject private Keyspace keyspaceOperator; @PostConstruct public void init() { friendsTemplate = new ThriftColumnFamilyTemplate(keyspaceOperator, getFriendsCF(), StringSerializer.get(), StringSerializer.get()); friendsTemplate.setCount(Constants.CASSANDRA_MAX_COLUMNS); } void addFriend(String key, String friendKey) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(key, getFriendsCF(), HFactory.createColumn(friendKey, Calendar.getInstance().getTimeInMillis(), StringSerializer.get(), LongSerializer.get())); } void removeFriend(String key, String friendKey) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(key, getFriendsCF(), friendKey, StringSerializer.get()); } List findFriends(String key) { ColumnFamilyResult result = friendsTemplate.queryColumns(key); List friends = new ArrayList(); for (String columnName : result.getColumnNames()) { friends.add(columnName); } return friends; } protected abstract String getFriendsCF(); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/AbstractCassandraLineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.status.Share; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.serializers.UUIDSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.ColumnQuery; import me.prettyprint.hector.api.query.QueryResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.UUID; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * This abstract class contains commun functions for Timeline and Userline. *

    * Timeline and Userline have the same structure : * - Key : login * - Name : status Id * - Value : "" * * @author Julien Dubois */ public abstract class AbstractCassandraLineRepository { private final Logger log = LoggerFactory.getLogger(AbstractCassandraLineRepository.class); @Inject protected Keyspace keyspaceOperator; /** * Add a status to the CF. */ protected void addStatus(String key, String cf, String statusId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(key, cf, HFactory.createColumn(UUID.fromString(statusId), "", UUIDSerializer.get(), StringSerializer.get())); } /** * Add a status with a time-to-live. */ protected void addStatus(String key, String cf, String statusId, int ttl) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(key, cf, HFactory.createColumn(UUID.fromString(statusId), "", ttl, UUIDSerializer.get(), StringSerializer.get())); } /** * Remove a collection of statuses. */ protected void removeStatuses(String key, String cf, Collection statusIdsToDelete) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); for (String statusId : statusIdsToDelete) { mutator.addDeletion(key, cf, UUID.fromString(statusId), UUIDSerializer.get()); } mutator.execute(); } List getLineFromCF(String cf, String login, int size, String start, String finish) { List> result; if (finish != null) { ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()) .setColumnFamily(cf) .setKey(login) .setRange(UUID.fromString(finish), null, true, size) .execute() .get(); result = query.getColumns().subList(1, query.getColumns().size()); } else if (start != null) { ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()) .setColumnFamily(cf) .setKey(login) .setRange(null, UUID.fromString(start), true, size) .execute() .get(); int maxIndex = query.getColumns().size() - 1; if (maxIndex < 0) { maxIndex = 0; } result = query.getColumns().subList(0, maxIndex); } else { ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()) .setColumnFamily(cf) .setKey(login) .setRange(null, null, true, size) .execute() .get(); result = query.getColumns(); } List line = new ArrayList(); for (HColumn column : result) { line.add(column.getName().toString()); } return line; } void shareStatus(String login, Share share, String columnFamily, String sharesColumnFamily) { QueryResult> isStatusAlreadyinTimeline = findByLoginAndStatusId(columnFamily, login, UUID.fromString(share.getOriginalStatusId())); if (isStatusAlreadyinTimeline.get() == null) { QueryResult> isStatusAlreadyShared = findByLoginAndStatusId(sharesColumnFamily, login, UUID.fromString(share.getOriginalStatusId())); if (isStatusAlreadyShared.get() == null) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(login, columnFamily, HFactory.createColumn(UUID.fromString(share.getStatusId()), "", UUIDSerializer.get(), StringSerializer.get())); mutator.insert(login, sharesColumnFamily, HFactory.createColumn(UUID.fromString(share.getOriginalStatusId()), "", UUIDSerializer.get(), StringSerializer.get())); } else { log.debug("Shared status {} is already shared in {}", share.getOriginalStatusId(), columnFamily); } } else { log.debug("Shared status {} is already present in {}", share.getOriginalStatusId(), columnFamily); } } QueryResult> findByLoginAndStatusId(String columnFamily, String login, UUID statusId) { ColumnQuery columnQuery = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()); columnQuery.setColumnFamily(columnFamily).setKey(login).setName(statusId); return columnQuery.execute(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraAppleDeviceRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.AppleDeviceRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the AppleDevice repository. *

    * Maps users to Apple device ids. *

    * Structure : * - Key = login * - Name = apple device id * - Value = "" * * @author Julien Dubois */ @Repository public class CassandraAppleDeviceRepository implements AppleDeviceRepository { private final Logger log = LoggerFactory.getLogger(CassandraAppleDeviceRepository.class); @Inject private Keyspace keyspaceOperator; @Override public void createAppleDevice(String login, String deviceId) { log.debug("Creating Apple Device for user {} : {}", login, deviceId); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(login, ColumnFamilyKeys.APPLE_DEVICE_CF, HFactory.createColumn(deviceId, "", StringSerializer.get(), StringSerializer.get())); } @Override public void removeAppleDevice(String login, String deviceId) { log.debug("Deleting Apple Device for user {} : {}", login, deviceId); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(login, ColumnFamilyKeys.APPLE_DEVICE_CF, deviceId, StringSerializer.get()); } @Override public Collection findAppleDevices(String login) { Collection deviceIds = new ArrayList(); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(ColumnFamilyKeys.APPLE_DEVICE_CF) .setKey(login) .setRange(null, null, false, Integer.MAX_VALUE) .execute() .get(); for (HColumn column : result.getColumns()) { deviceIds.add(column.getName()); } return deviceIds; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraAppleDeviceUserRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.AppleDeviceUserRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.ColumnQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import javax.inject.Inject; import static fr.ippon.tatami.config.ColumnFamilyKeys.APPLE_DEVICE_USER_CF; /** * Cassandra implementation of the AppleDeviceUser repository. *

    * Maps Apple device ids to users. *

    * Structure : * - Key = apple device id * - Name = USER_LOGIN * - Value = user login * * @author Julien Dubois */ @Repository public class CassandraAppleDeviceUserRepository implements AppleDeviceUserRepository { private final Logger log = LoggerFactory.getLogger(CassandraAppleDeviceUserRepository.class); private static final String USER_LOGIN = "USER_LOGIN"; @Inject private Keyspace keyspaceOperator; @Override public void createAppleDeviceForUser(String deviceId, String login) { log.debug("Mapping Apple device id to user {} : {}", deviceId, login); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(deviceId, APPLE_DEVICE_USER_CF, HFactory.createColumn(USER_LOGIN, login, StringSerializer.get(), StringSerializer.get())); } @Override public void removeAppleDeviceForUser(String deviceId) { log.debug("Removing mapping of Apple device id {}", deviceId); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addDeletion(deviceId, APPLE_DEVICE_USER_CF); mutator.execute(); } @Override public String findLoginForDeviceId(String deviceId) { log.debug("Finding user of Apple device id {}", deviceId); ColumnQuery query = HFactory.createStringColumnQuery(keyspaceOperator); HColumn column = query.setColumnFamily(APPLE_DEVICE_USER_CF) .setKey(deviceId) .setName(USER_LOGIN) .execute() .get(); if (column != null) { return column.getValue(); } else { return null; } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraAttachmentRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.Attachment; import fr.ippon.tatami.repository.AttachmentRepository; import me.prettyprint.cassandra.serializers.BytesArraySerializer; import me.prettyprint.cassandra.serializers.DateSerializer; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.utils.TimeUUIDUtils; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.ColumnQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.Date; import static fr.ippon.tatami.config.ColumnFamilyKeys.ATTACHMENT_CF; @Repository public class CassandraAttachmentRepository implements AttachmentRepository { private final Logger log = LoggerFactory.getLogger(CassandraAttachmentRepository.class); private final String CONTENT = "content"; private final String THUMBNAIL = "thumbnail"; private final String FILENAME = "filename"; private final String SIZE = "size"; private final String CREATION_DATE = "creation_date"; @Inject private Keyspace keyspaceOperator; @Override public void createAttachment(Attachment attachment) { String attachmentId = TimeUUIDUtils.getUniqueTimeUUIDinMillis().toString(); log.debug("Creating attachment : {}", attachment); attachment.setAttachmentId(attachmentId); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(attachmentId, ATTACHMENT_CF, HFactory.createColumn(CONTENT, attachment.getContent(), StringSerializer.get(), BytesArraySerializer.get())); mutator.insert(attachmentId, ATTACHMENT_CF, HFactory.createColumn(THUMBNAIL, attachment.getThumbnail(), StringSerializer.get(), BytesArraySerializer.get())); mutator.insert(attachmentId, ATTACHMENT_CF, HFactory.createColumn(FILENAME, attachment.getFilename(), StringSerializer.get(), StringSerializer.get())); mutator.insert(attachmentId, ATTACHMENT_CF, HFactory.createColumn(SIZE, attachment.getSize(), StringSerializer.get(), LongSerializer.get())); mutator.insert(attachmentId, ATTACHMENT_CF, HFactory.createColumn(CREATION_DATE, attachment.getCreationDate(), StringSerializer.get(), DateSerializer.get())); } @Override @CacheEvict(value = "attachment-cache", key = "#attachment.attachmentId") public void deleteAttachment(Attachment attachment) { log.debug("Deleting attachment : {}", attachment); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addDeletion(attachment.getAttachmentId(), ATTACHMENT_CF); mutator.execute(); } @Override @Cacheable("attachment-cache") public Attachment findAttachmentById(String attachmentId) { if (attachmentId == null) { return null; } log.debug("Finding attachment : {}", attachmentId); Attachment attachment = this.findAttachmentMetadataById(attachmentId); if (attachment == null) { return null; } ColumnQuery queryAttachment = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), BytesArraySerializer.get()); HColumn columnAttachment = queryAttachment.setColumnFamily(ATTACHMENT_CF) .setKey(attachmentId) .setName(CONTENT) .execute() .get(); attachment.setContent(columnAttachment.getValue()); ColumnQuery queryThumbnail = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), BytesArraySerializer.get()); HColumn columnThumbnail = queryThumbnail.setColumnFamily(ATTACHMENT_CF) .setKey(attachmentId) .setName(THUMBNAIL) .execute() .get(); if(columnThumbnail != null && columnThumbnail.getValue().length > 0) { attachment.setThumbnail(columnThumbnail.getValue()); attachment.setHasThumbnail(true); } else { attachment.setHasThumbnail(false); } return attachment; } @Override public Attachment findAttachmentMetadataById(String attachmentId) { if (attachmentId == null) { return null; } Attachment attachment = new Attachment(); attachment.setAttachmentId(attachmentId); ColumnQuery queryFilename = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()); HColumn columnFilename = queryFilename.setColumnFamily(ATTACHMENT_CF) .setKey(attachmentId) .setName(FILENAME) .execute() .get(); if (columnFilename != null && columnFilename.getValue() != null) { attachment.setFilename(columnFilename.getValue()); } else { return null; } ColumnQuery querySize = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), LongSerializer.get()); HColumn columnSize = querySize.setColumnFamily(ATTACHMENT_CF) .setKey(attachmentId) .setName(SIZE) .execute() .get(); if (columnSize != null && columnSize.getValue() != null) { attachment.setSize(columnSize.getValue()); } else { return null; } ColumnQuery queryCreationDate = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), DateSerializer.get()); HColumn columnCreationDate = queryCreationDate.setColumnFamily(ATTACHMENT_CF) .setKey(attachmentId) .setName(CREATION_DATE) .execute() .get(); if (columnCreationDate != null && columnCreationDate.getValue() != null) { attachment.setCreationDate(columnCreationDate.getValue()); } else { attachment.setCreationDate(new Date()); } return attachment; } @Override public Attachment updateThumbnail(Attachment attach) { log.debug("Updating thumbnail : {}", attach); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(attach.getAttachmentId(), ATTACHMENT_CF, HFactory.createColumn(THUMBNAIL, attach.getThumbnail(), StringSerializer.get(), BytesArraySerializer.get())); return attach; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraAvatarRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.Avatar; import fr.ippon.tatami.repository.AvatarRepository; import me.prettyprint.cassandra.serializers.BytesArraySerializer; import me.prettyprint.cassandra.serializers.DateSerializer; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.utils.TimeUUIDUtils; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.ColumnQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.Date; import static fr.ippon.tatami.config.ColumnFamilyKeys.AVATAR_CF; @Repository public class CassandraAvatarRepository implements AvatarRepository { private final Logger log = LoggerFactory.getLogger(CassandraAttachmentRepository.class); private final String CONTENT = "content"; private final String FILENAME = "filename"; private final String SIZE = "size"; private final String CREATION_DATE = "creation_date"; @Inject private Keyspace keyspaceOperator; @Override public void createAvatar(Avatar avatar) { String avatarId = TimeUUIDUtils.getUniqueTimeUUIDinMillis().toString(); log.debug("Creating avatar : {}", avatar); avatar.setAvatarId(avatarId); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(avatarId, AVATAR_CF, HFactory.createColumn(CONTENT, avatar.getContent(), StringSerializer.get(), BytesArraySerializer.get())); mutator.insert(avatarId, AVATAR_CF, HFactory.createColumn(FILENAME, avatar.getFilename(), StringSerializer.get(), StringSerializer.get())); mutator.insert(avatarId, AVATAR_CF, HFactory.createColumn(SIZE, avatar.getSize(), StringSerializer.get(), LongSerializer.get())); mutator.insert(avatarId, AVATAR_CF, HFactory.createColumn(CREATION_DATE, avatar.getCreationDate(), StringSerializer.get(), DateSerializer.get())); } @Override @CacheEvict(value = "avatar-cache") public void removeAvatar(String avatarId) { log.debug("Avatar deleted : {}", avatarId); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addDeletion(avatarId, AVATAR_CF); mutator.execute(); } @Override @Cacheable("avatar-cache") public Avatar findAvatarById(String avatarId) { if (avatarId == null) { return null; } log.debug("Finding avatar : {}", avatarId); Avatar avatar = this.findAttachmentMetadataById(avatarId); if (avatar == null) { return null; } ColumnQuery queryAttachment = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), BytesArraySerializer.get()); HColumn columnAttachment = queryAttachment.setColumnFamily(AVATAR_CF) .setKey(avatarId) .setName(CONTENT) .execute() .get(); avatar.setContent(columnAttachment.getValue()); return avatar; } Avatar findAttachmentMetadataById(String avatarId) { if (avatarId == null) { return null; } Avatar avatar = new Avatar(); avatar.setAvatarId(avatarId); ColumnQuery queryFilename = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()); HColumn columnFilename = queryFilename.setColumnFamily(AVATAR_CF) .setKey(avatarId) .setName(FILENAME) .execute() .get(); if (columnFilename != null && columnFilename.getValue() != null) { avatar.setFilename(columnFilename.getValue()); } else { return null; } ColumnQuery querySize = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), LongSerializer.get()); HColumn columnSize = querySize.setColumnFamily(AVATAR_CF) .setKey(avatarId) .setName(SIZE) .execute() .get(); if (columnSize != null && columnSize.getValue() != null) { avatar.setSize(columnSize.getValue()); } else { return null; } ColumnQuery queryCreationDate = HFactory.createColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), DateSerializer.get()); HColumn columnCreationDate = queryCreationDate.setColumnFamily(AVATAR_CF) .setKey(avatarId) .setName(CREATION_DATE) .execute() .get(); if (columnCreationDate != null && columnCreationDate.getValue() != null) { avatar.setCreationDate(columnCreationDate.getValue()); } else { avatar.setCreationDate(new Date()); } return avatar; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraBlockRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.repository.BlockRepository; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.service.template.ColumnFamilyResult; import me.prettyprint.cassandra.service.template.ColumnFamilyTemplate; import me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.List; import static fr.ippon.tatami.config.ColumnFamilyKeys.BLOCK_USERS_CF; /** * Created by matthieudelafourniere on 7/7/16. */ @Repository public class CassandraBlockRepository implements BlockRepository { private ColumnFamilyTemplate blockedUsersTemplate; @Inject private Keyspace keyspaceOperator; @PostConstruct public void init() { blockedUsersTemplate = new ThriftColumnFamilyTemplate(keyspaceOperator, BLOCK_USERS_CF, StringSerializer.get(), StringSerializer.get()); blockedUsersTemplate.setCount(Constants.CASSANDRA_MAX_COLUMNS); } @Override public void blockUser(String currentUserLogin, String blockedUserLogin) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(currentUserLogin, BLOCK_USERS_CF, HFactory.createColumn(blockedUserLogin, Calendar.getInstance().getTimeInMillis(), StringSerializer.get(), LongSerializer.get())); } @Override public void unblockUser(String currentUserLogin, String unblockedUserLogin) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(currentUserLogin, BLOCK_USERS_CF, unblockedUserLogin, StringSerializer.get()); } @Override public Collection getUsersBlockedBy(String userLogin) { ColumnFamilyResult result = blockedUsersTemplate.queryColumns(userLogin); Collection blockedUsers = new ArrayList(); for (String columnName : result.getColumnNames()) { blockedUsers.add(columnName); } return blockedUsers; } @Override public boolean isBlocked(String blockingLogin, String blockedLogin) { Collection blockedEmails = getUsersBlockedBy(blockingLogin); return blockedEmails.contains(blockedLogin); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraCounterRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.CounterRepository; import me.prettyprint.cassandra.model.thrift.ThriftCounterColumnQuery; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.HCounterColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.CounterQuery; import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import javax.inject.Inject; import static me.prettyprint.hector.api.factory.HFactory.createCounterColumn; /** * Cassandra implementation of the Counter repository. *

    * Structure : * - Key = login * - Name = counterId * - Value = count * * @author Julien Dubois */ @Repository public class CassandraCounterRepository implements CounterRepository { private static final String STATUS_COUNTER = "STATUS_COUNTER"; private static final String FOLLOWERS_COUNTER = "FOLLOWERS_COUNTER"; private static final String FRIENDS_COUNTER = "FRIENDS_COUNTER"; @Inject private Keyspace keyspaceOperator; @Override @CacheEvict(value = "user-cache", key = "#login") public void incrementFollowersCounter(String login) { incrementCounter(FOLLOWERS_COUNTER, login); } @Override @CacheEvict(value = {"user-cache", "suggest-users-cache"}, key = "#login") public void incrementFriendsCounter(String login) { incrementCounter(FRIENDS_COUNTER, login); } @Override @CacheEvict(value = "user-cache", key = "#login") public void incrementStatusCounter(String login) { incrementCounter(STATUS_COUNTER, login); } @Override @CacheEvict(value = "user-cache", key = "#login") public void decrementFollowersCounter(String login) { decrementCounter(FOLLOWERS_COUNTER, login); } @Override @CacheEvict(value = "user-cache", key = "#login") public void decrementFriendsCounter(String login) { decrementCounter(FRIENDS_COUNTER, login); } @Override @CacheEvict(value = "user-cache", key = "#login") public void decrementStatusCounter(String login) { decrementCounter(STATUS_COUNTER, login); } @Override public long getFollowersCounter(String login) { return getCounter(FOLLOWERS_COUNTER, login); } @Override public long getFriendsCounter(String login) { return getCounter(FRIENDS_COUNTER, login); } @Override public long getStatusCounter(String login) { return getCounter(STATUS_COUNTER, login); } @Override public void createFollowersCounter(String login) { createCounter(FOLLOWERS_COUNTER, login); } @Override public void createFriendsCounter(String login) { createCounter(FRIENDS_COUNTER, login); } @Override public void createStatusCounter(String login) { createCounter(STATUS_COUNTER, login); } @Override public void deleteCounters(String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addCounterDeletion(login, ColumnFamilyKeys.COUNTER_CF, STATUS_COUNTER, StringSerializer.get()); mutator.addCounterDeletion(login, ColumnFamilyKeys.COUNTER_CF, FOLLOWERS_COUNTER, StringSerializer.get()); mutator.addCounterDeletion(login, ColumnFamilyKeys.COUNTER_CF, FRIENDS_COUNTER, StringSerializer.get()); mutator.execute(); } private void createCounter(String counterName, String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insertCounter(login, ColumnFamilyKeys.COUNTER_CF, createCounterColumn(counterName, 0)); } private void incrementCounter(String counterName, String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.incrementCounter(login, ColumnFamilyKeys.COUNTER_CF, counterName, 1); } private void decrementCounter(String counterName, String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.decrementCounter(login, ColumnFamilyKeys.COUNTER_CF, counterName, 1); } private long getCounter(String counterName, String login) { CounterQuery counter = new ThriftCounterColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get()); counter.setColumnFamily(ColumnFamilyKeys.COUNTER_CF).setKey(login).setName(counterName); HCounterColumn counterColumn = counter.execute().get(); if (counterColumn == null) { return 0; } else { return counterColumn.getValue(); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraDaylineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.UserStatusStat; import fr.ippon.tatami.domain.status.Status; import fr.ippon.tatami.repository.DaylineRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.CounterSlice; import me.prettyprint.hector.api.beans.HCounterColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.SliceCounterQuery; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.Collection; import java.util.TreeSet; import static fr.ippon.tatami.config.ColumnFamilyKeys.DAYLINE_CF; import static me.prettyprint.hector.api.factory.HFactory.createCounterSliceQuery; /** * Cassandra implementation of the user repository. *

    * Structure : * - Key = day + domain * - Name = username * - Value = count * * @author Julien Dubois */ @Repository public class CassandraDaylineRepository implements DaylineRepository { @Inject private Keyspace keyspaceOperator; @Override public void addStatusToDayline(Status status, String day) { String key = getKey(status.getDomain(), day); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.incrementCounter(key, DAYLINE_CF, status.getUsername(), 1); } @Override @Cacheable("dayline-cache") public Collection getDayline(String domain, String day) { String key = getKey(domain, day); Collection results = new TreeSet(); SliceCounterQuery query = createCounterSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get()) .setColumnFamily(DAYLINE_CF) .setRange(null, null, false, Integer.MAX_VALUE) .setKey(key); CounterSlice queryResult = query.execute().get(); for (HCounterColumn column : queryResult.getColumns()) { UserStatusStat stat = new UserStatusStat(column.getName(), column.getValue()); results.add(stat); } return results; } /** * Generates the key for this column family. */ private String getKey(String domain, String day) { return day + "-" + domain; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraDiscussionRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.DiscussionRepository; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.Calendar; import java.util.Collection; import java.util.LinkedHashSet; import static fr.ippon.tatami.config.ColumnFamilyKeys.DISCUSSION_CF; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the StatusDetails repository. *

    * Structure : * - Key = originial status Id * - Name = time * - Value = reply status Id * * @author Julien Dubois */ @Repository public class CassandraDiscussionRepository implements DiscussionRepository { @Inject private Keyspace keyspaceOperator; @Override @CacheEvict(value = "status-cache", key = "#originalStatusId") public void addReplyToDiscussion(String originalStatusId, String replyStatusId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(originalStatusId, DISCUSSION_CF, HFactory.createColumn( Calendar.getInstance().getTimeInMillis(), replyStatusId, LongSerializer.get(), StringSerializer.get())); } @Override public Collection findStatusIdsInDiscussion(String originalStatusId) { ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), LongSerializer.get(), StringSerializer.get()) .setColumnFamily(DISCUSSION_CF) .setKey(originalStatusId) .setRange(null, null, false, Integer.MAX_VALUE) .execute() .get(); Collection statusIds = new LinkedHashSet(); for (HColumn column : result.getColumns()) { statusIds.add(column.getValue()); } return statusIds; } @Override public boolean hasReply(String statusId) { int zeroOrOne = HFactory.createCountQuery(keyspaceOperator, StringSerializer.get(), LongSerializer.get()) .setColumnFamily(DISCUSSION_CF) .setKey(statusId) .setRange(null, null, 1) .execute() .get(); return zeroOrOne > 0; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraDomainConfigurationRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import javax.inject.Inject; import me.prettyprint.hom.EntityManagerImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import fr.ippon.tatami.domain.DomainConfiguration; import fr.ippon.tatami.repository.DomainConfigurationRepository; /** * Cassandra implementation of the DomainConfiguration repository. * * @author Julien Dubois */ @Repository public class CassandraDomainConfigurationRepository implements DomainConfigurationRepository { private final Logger log = LoggerFactory.getLogger(CassandraDomainConfigurationRepository.class); @Inject private EntityManagerImpl em; @Override public void updateDomainConfiguration(DomainConfiguration domainConfiguration) { setDefaultValues(domainConfiguration); em.persist(domainConfiguration); } @Override public DomainConfiguration findDomainConfigurationByDomain(String domain) { DomainConfiguration domainConfiguration; try { domainConfiguration = em.find(DomainConfiguration.class, domain); } catch (Exception e) { log.debug("Exception while looking for domain {} : {}", domain, e.toString()); return null; } if (domainConfiguration == null) { domainConfiguration = new DomainConfiguration(); domainConfiguration.setDomain(domain); setDefaultValues(domainConfiguration); em.persist(domainConfiguration); } if (domain.equals("ippon.fr")) { domainConfiguration.setSubscriptionLevel(DomainConfiguration.SubscriptionAndStorageSizeOptions.IPPONSUSCRIPTION); domainConfiguration.setStorageSize(DomainConfiguration.SubscriptionAndStorageSizeOptions.IPPONSIZE); } return domainConfiguration; } private void setDefaultValues(DomainConfiguration domainConfiguration) { if (domainConfiguration.getStorageSize() == null) { domainConfiguration.setStorageSize(DomainConfiguration.SubscriptionAndStorageSizeOptions.BASICSIZE); } if (domainConfiguration.getSubscriptionLevel() == null) { domainConfiguration.setSubscriptionLevel(DomainConfiguration.SubscriptionAndStorageSizeOptions.BASICSUSCRIPTION); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraDomainRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.domain.Domain; import fr.ippon.tatami.repository.DomainRepository; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.beans.OrderedRows; import me.prettyprint.hector.api.beans.Row; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.QueryResult; import me.prettyprint.hector.api.query.RangeSlicesQuery; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.*; import static fr.ippon.tatami.config.ColumnFamilyKeys.DOMAIN_CF; import static me.prettyprint.hector.api.factory.HFactory.createRangeSlicesQuery; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the Domain repository. *

    * Structure : * - Key = domain * - Name = login * - Value = time * * @author Julien Dubois */ @Repository public class CassandraDomainRepository implements DomainRepository { @Inject private Keyspace keyspaceOperator; @Override public void addUserInDomain(String domain, String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(domain, DOMAIN_CF, HFactory.createColumn(login, Calendar.getInstance().getTimeInMillis(), StringSerializer.get(), LongSerializer.get())); } @Override public void updateUserInDomain(String domain, String login) { this.addUserInDomain(domain, login); } @Override public void deleteUserInDomain(String domain, String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(domain, DOMAIN_CF, login, StringSerializer.get()); } @Override public List getLoginsInDomain(String domain, int pagination) { int maxColumns = pagination + Constants.PAGINATION_SIZE; List logins = new ArrayList(); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(DOMAIN_CF) .setKey(domain) .setRange(null, null, false, maxColumns) .execute() .get(); int index = 0; for (HColumn column : result.getColumns()) { // We take one more item, to display (or not) the "next" button if there is an item after the displayed list. if (index > maxColumns) { break; } if (index >= pagination) { logins.add(column.getName()); } index++; } return logins; } @Override public List getLoginsInDomain(String domain) { List logins = new ArrayList(); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(DOMAIN_CF) .setKey(domain) .setRange(null, null, false, Integer.MAX_VALUE) .execute() .get(); for (HColumn column : result.getColumns()) { logins.add(column.getName()); } return logins; } @Override public Set getAllDomains() { Set domains = new HashSet(); RangeSlicesQuery query = createRangeSlicesQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(DOMAIN_CF) .setRange(null, null, false, Integer.MAX_VALUE) .setRowCount(Constants.CASSANDRA_MAX_ROWS); QueryResult> result = query.execute(); List> rows = result.get().getList(); for (Row row : rows) { Domain domain = new Domain(); domain.setName(row.getKey()); domain.setNumberOfUsers(row.getColumnSlice().getColumns().size()); domains.add(domain); } return domains; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraDomainlineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.DomainlineRepository; import me.prettyprint.hector.api.Keyspace; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.Collection; import java.util.List; import static fr.ippon.tatami.config.ColumnFamilyKeys.DOMAINLINE_CF; /** * Cassandra implementation of the Domain line repository. *

    * Structure : * - Key = domain * - Name = statusId * - Value = "" * * @author Julien Dubois */ @Repository public class CassandraDomainlineRepository extends AbstractCassandraLineRepository implements DomainlineRepository { private final static int COLUMN_TTL = 60 * 60 * 24 * 30; // The column is stored for 30 days. @Inject private Keyspace keyspaceOperator; @Override public void addStatusToDomainline(String domain, String statusId) { addStatus(domain, DOMAINLINE_CF, statusId, COLUMN_TTL); } @Override public void removeStatusFromDomainline(String domain, Collection statusIdsToDelete) { removeStatuses(domain, DOMAINLINE_CF, statusIdsToDelete); } @Override public List getDomainline(String domain, int size, String start, String finish) { return getLineFromCF(DOMAINLINE_CF, domain, size, start, finish); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraFavoritelineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.FavoritelineRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.serializers.UUIDSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.ArrayList; import java.util.List; import java.util.UUID; import static fr.ippon.tatami.config.ColumnFamilyKeys.FAVLINE_CF; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the favoriteline repository. *

    * Structure : * - Key = login * - Name = statusId * - Value = "" * * @author Julien Dubois */ @Repository public class CassandraFavoritelineRepository implements FavoritelineRepository { @Inject private Keyspace keyspaceOperator; @Override @CacheEvict(value = "favorites-cache", key = "#login") public void addStatusToFavoriteline(String login, String statusId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(login, FAVLINE_CF, HFactory.createColumn(UUID.fromString(statusId), "", UUIDSerializer.get(), StringSerializer.get())); } @Override @CacheEvict(value = "favorites-cache", key = "#login") public void removeStatusFromFavoriteline(String login, String statusId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(login, FAVLINE_CF, UUID.fromString(statusId), UUIDSerializer.get()); } @Override @Cacheable("favorites-cache") public List getFavoriteline(String login) { List line = new ArrayList(); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()) .setColumnFamily(FAVLINE_CF) .setKey(login) .setRange(null, null, true, 50) .execute() .get(); for (HColumn column : result.getColumns()) { line.add(column.getName().toString()); } return line; } @Override public void deleteFavoriteline(String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addDeletion(login, FAVLINE_CF); mutator.execute(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraFollowerRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.FollowerRepository; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import java.util.Collection; /** * Cassandra implementation of the Follower repository. *

    * Structure : * - Key = login * - Name = follower login * - Value = time * * @author Julien Dubois */ @Repository public class CassandraFollowerRepository extends AbstractCassandraFollowerRepository implements FollowerRepository { @Override @CacheEvict(value = "followers-cache", key = "#login") public void addFollower(String login, String followerLogin) { super.addFollower(login, followerLogin); } @Override @CacheEvict(value = "followers-cache", key = "#login") public void removeFollower(String login, String followerLogin) { super.removeFollower(login, followerLogin); } @Override @Cacheable("followers-cache") public Collection findFollowersForUser(String login) { return super.findFollowers(login); } @Override public String getFollowersCF() { return ColumnFamilyKeys.FOLLOWERS_CF; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraFriendRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.FriendRepository; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import java.util.List; import static fr.ippon.tatami.config.ColumnFamilyKeys.FRIENDS_CF; /** * Cassandra implementation of the Friend repository. *

    * Structure : * - Key = login * - Name = friend login * - Value = time * * @author Julien Dubois */ @Repository public class CassandraFriendRepository extends AbstractCassandraFriendRepository implements FriendRepository { @Override @CacheEvict(value = "friends-cache", key = "#login") public void addFriend(String login, String friendLogin) { super.addFriend(login, friendLogin); } @Override @CacheEvict(value = "friends-cache", key = "#login") public void removeFriend(String login, String friendLogin) { super.removeFriend(login, friendLogin); } @Override @Cacheable("friends-cache") public List findFriendsForUser(String login) { return super.findFriends(login); } @Override public String getFriendsCF() { return FRIENDS_CF; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraGroupCounterRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.GroupCounterRepository; import me.prettyprint.cassandra.model.thrift.ThriftCounterColumnQuery; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.CounterQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import javax.inject.Inject; import static fr.ippon.tatami.config.ColumnFamilyKeys.GROUP_COUNTER_CF; /** * Cassandra implementation of the Group Counter repository. *

    * Structure : * - Key = domain * - Name = groupId * - Value = count * * @author Julien Dubois */ @Repository public class CassandraGroupCounterRepository implements GroupCounterRepository { @Inject private Keyspace keyspaceOperator; @Override public long getGroupCounter(String domain, String groupId) { CounterQuery counter = new ThriftCounterColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get()); counter.setColumnFamily(GROUP_COUNTER_CF).setKey(domain).setName(groupId); return counter.execute().get().getValue(); } protected final Logger log = LoggerFactory.getLogger(this.getClass().getCanonicalName()); @Override public void incrementGroupCounter(String domain, String groupId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.incrementCounter(domain, GROUP_COUNTER_CF, groupId, 1); } @Override public void decrementGroupCounter(String domain, String groupId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.decrementCounter(domain, GROUP_COUNTER_CF, groupId, 1); } @Override public void deleteGroupCounter(String domain, String groupId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addCounterDeletion(domain, GROUP_COUNTER_CF, groupId, StringSerializer.get()); mutator.execute(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraGroupDetailsRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.repository.GroupDetailsRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import javax.inject.Inject; import static fr.ippon.tatami.config.ColumnFamilyKeys.GROUP_DETAILS_CF; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the Group Details repository. *

    * Structure : * - Key = Group ID * - Name / Value pairs of group details * * @author Julien Dubois */ @Repository public class CassandraGroupDetailsRepository implements GroupDetailsRepository { private static final String NAME = "name"; private static final String DESCRIPTION = "description"; private static final String PUBLIC_GROUP = "publicGroup"; private static final String ARCHIVED_GROUP = "archivedGroup"; @Inject private Keyspace keyspaceOperator; @Override public void createGroupDetails(String groupId, String name, String description, boolean publicGroup) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(groupId, GROUP_DETAILS_CF, HFactory.createColumn(NAME, name, StringSerializer.get(), StringSerializer.get())); mutator.insert(groupId, GROUP_DETAILS_CF, HFactory.createColumn(DESCRIPTION, description, StringSerializer.get(), StringSerializer.get())); mutator.insert(groupId, GROUP_DETAILS_CF, HFactory.createColumn(PUBLIC_GROUP, (Boolean.valueOf(publicGroup)).toString(), StringSerializer.get(), StringSerializer.get())); mutator.insert(groupId, GROUP_DETAILS_CF, HFactory.createColumn(ARCHIVED_GROUP, Boolean.FALSE.toString(), StringSerializer.get(), StringSerializer.get())); } @Override public void editGroupDetails(String groupId, String name, String description, boolean archivedGroup) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(groupId, GROUP_DETAILS_CF, HFactory.createColumn(NAME, name, StringSerializer.get(), StringSerializer.get())); mutator.insert(groupId, GROUP_DETAILS_CF, HFactory.createColumn(DESCRIPTION, description, StringSerializer.get(), StringSerializer.get())); mutator.insert(groupId, GROUP_DETAILS_CF, HFactory.createColumn(ARCHIVED_GROUP, (Boolean.valueOf(archivedGroup)).toString(), StringSerializer.get(), StringSerializer.get())); } @Override public Group getGroupDetails(String groupId) { Group group = new Group(); group.setGroupId(groupId); group.setPublicGroup(false); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(GROUP_DETAILS_CF) .setKey(groupId) .setRange(null, null, false, 4) .execute() .get(); for (HColumn column : result.getColumns()) { if (column.getName().equals(NAME)) { group.setName(column.getValue()); } else if (column.getName().equals(DESCRIPTION)) { group.setDescription(column.getValue()); } else if (column.getName().equals(PUBLIC_GROUP)) { if (column.getValue().equals(Boolean.TRUE.toString())) { group.setPublicGroup(true); } } else if (column.getName().equals(ARCHIVED_GROUP)) { if (column.getValue().equals(Boolean.TRUE.toString())) { group.setArchivedGroup(true); } } } return group; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraGroupMembersRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.GroupRoles; import fr.ippon.tatami.repository.GroupMembersRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.HashMap; import java.util.Map; import static fr.ippon.tatami.config.ColumnFamilyKeys.GROUP_MEMBERS_CF; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the Group members repository. *

    * Structure : * - Key = group ID * - Name = login * - Value = role * * @author Julien Dubois */ @Repository public class CassandraGroupMembersRepository implements GroupMembersRepository { @Inject private Keyspace keyspaceOperator; @Override public void addMember(String groupId, String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(groupId, GROUP_MEMBERS_CF, HFactory.createColumn(login, GroupRoles.MEMBER, StringSerializer.get(), StringSerializer.get())); } @Override public void addAdmin(String groupId, String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(groupId, GROUP_MEMBERS_CF, HFactory.createColumn(login, GroupRoles.ADMIN, StringSerializer.get(), StringSerializer.get())); } @Override public void removeMember(String groupId, String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(groupId, GROUP_MEMBERS_CF, login, StringSerializer.get()); } @Override public Map findMembers(String groupId) { Map members = new HashMap(); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(GROUP_MEMBERS_CF) .setKey(groupId) .setRange(null, null, false, Integer.MAX_VALUE) .execute() .get(); for (HColumn column : result.getColumns()) { members.put(column.getName(), column.getValue()); } return members; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraGroupRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.repository.GroupRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.utils.TimeUUIDUtils; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.ColumnQuery; import org.springframework.stereotype.Repository; import javax.inject.Inject; import static fr.ippon.tatami.config.ColumnFamilyKeys.GROUP_CF; /** * Cassandra implementation of the Group repository. *

    * Structure : * - Key = domain * - Name = Group ID * - Value = "" * * @author Julien Dubois */ @Repository public class CassandraGroupRepository implements GroupRepository { @Inject private Keyspace keyspaceOperator; @Override public String createGroup(String domain) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); String groupId = TimeUUIDUtils.getUniqueTimeUUIDinMillis().toString(); mutator.insert(domain, GROUP_CF, HFactory.createColumn(groupId, "", StringSerializer.get(), StringSerializer.get())); return groupId; } @Override public Group getGroupById(String domain, String groupId) { ColumnQuery query = HFactory.createStringColumnQuery(keyspaceOperator); HColumn column = query.setColumnFamily(GROUP_CF) .setKey(domain) .setName(groupId) .execute() .get(); if (column != null) { Group group = new Group(); group.setDomain(domain); group.setGroupId(groupId); return group; } else { return null; } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraGrouplineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.GrouplineRepository; import me.prettyprint.hector.api.Keyspace; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import javax.inject.Inject; import java.util.Collection; import java.util.List; /** * Cassandra implementation of the Group line repository. *

    * Structure : * - Key = groupId * - Name = statusId * - Value = "" * * @author Julien Dubois */ @Repository public class CassandraGrouplineRepository extends AbstractCassandraLineRepository implements GrouplineRepository { @Inject private Keyspace keyspaceOperator; @Override public void addStatusToGroupline(String groupId, String statusId) { addStatus(groupId, ColumnFamilyKeys.GROUPLINE_CF, statusId); } @Override public void removeStatusesFromGroupline(String groupId, Collection statusIdsToDelete) { removeStatuses(groupId, ColumnFamilyKeys.GROUPLINE_CF, statusIdsToDelete); } @Override public List getGroupline(String groupId, int size, String start, String finish) { return getLineFromCF(ColumnFamilyKeys.GROUPLINE_CF, groupId, size, start, finish); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraIdempotentRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.ColumnQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import fr.ippon.tatami.repository.IdempotentRepository; import javax.inject.Inject; /** * Used to de-deplucate Camel messages. */ @Component public class CassandraIdempotentRepository implements IdempotentRepository { private final Logger log = LoggerFactory.getLogger(CassandraIdempotentRepository.class); private final static String KEY = "Default"; private final static String TATAMIBOT_DUPLICATE_CF = "TatamiBotDuplicate"; private final static int COLUMN_TTL = 60 * 60 * 24 * 30; // The column is stored for 30 days. @Inject private Keyspace keyspaceOperator; @Override public boolean add(String key) { if (contains(key)) { log.debug("Duplicate message detected!"); return false; } else { log.debug("Adding new message to the idempotent repository"); HColumn column = HFactory.createColumn( key, "", COLUMN_TTL, StringSerializer.get(), StringSerializer.get()); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(KEY, TATAMIBOT_DUPLICATE_CF, column); return true; } } @Override public boolean contains(String key) { log.debug("Test message duplication with key : {}", key); ColumnQuery query = HFactory.createStringColumnQuery(keyspaceOperator); HColumn column = query.setColumnFamily(TATAMIBOT_DUPLICATE_CF) .setKey(KEY) .setName(key) .execute() .get(); return column != null; } @Override public boolean remove(String key) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(KEY, TATAMIBOT_DUPLICATE_CF, key, StringSerializer.get()); return true; } @Override public boolean confirm(String key) { return true; // noop } @Override public void start() throws Exception { } @Override public void stop() throws Exception { } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraMailDigestRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.domain.DigestType; import fr.ippon.tatami.repository.MailDigestRepository; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import javax.inject.Inject; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * MailDigestRepository implementation for cassandra *

    * Structure : * - Key = digestType_[day]_domain * - Name = login * - Value = time *

    * Note : in the key, the [day] part is only used for weekly digest and * represents the day the user subscribed to the digest. * * @author Pierre Rust */ @Repository public class CassandraMailDigestRepository implements MailDigestRepository { @Inject private Keyspace keyspaceOperator; @Override public void subscribeToDigest(DigestType digestType, String login, String domain, String day) { Calendar cal = Calendar.getInstance(); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(buildKey(digestType, domain, day), ColumnFamilyKeys.MAILDIGEST_CF, HFactory.createColumn(login, cal.getTimeInMillis(), StringSerializer.get(), LongSerializer.get())); } @Override public void unsubscribeFromDigest(DigestType digestType, String login, String domain, String day) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(buildKey(digestType, domain, day), ColumnFamilyKeys.MAILDIGEST_CF, login, StringSerializer.get()); } @Override public List getLoginsRegisteredToDigest(DigestType digestType, String domain, String day, int pagination) { List logins = new ArrayList(); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(ColumnFamilyKeys.MAILDIGEST_CF) .setKey(buildKey(digestType, domain, day)) .setRange(null, null, false, Integer.MAX_VALUE) .execute() .get(); int index = 0; for (HColumn column : result.getColumns()) { // We take one more item, to display (or not) the "next" button if there is an item after the displayed list. if (index > pagination + Constants.PAGINATION_SIZE) { break; } if (index >= pagination) { logins.add(column.getName()); } index++; } return logins; } /** * @return the row key */ private String buildKey(DigestType digestType, String domain, String day) { String key; if (DigestType.WEEKLY_DIGEST == digestType) { key = digestType.toString() + "_" + day + "_" + domain; } else { key = digestType + "_" + domain; } return key; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraMentionlineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.MentionlineRepository; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import java.util.Collection; import java.util.List; /** * Cassandra implementation of the Userline repository. *

    * Structure : * - Key : login * - Name : status Id * - Value : "" * * @author Julien Dubois */ @Repository public class CassandraMentionlineRepository extends AbstractCassandraLineRepository implements MentionlineRepository { @Override public void addStatusToMentionline(String mentionedLogin, String statusId) { addStatus(mentionedLogin, ColumnFamilyKeys.MENTIONLINE_CF, statusId); } @Override public void removeStatusesFromMentionline(String mentionedLogin, Collection statusIdsToDelete) { removeStatuses(mentionedLogin, ColumnFamilyKeys.MENTIONLINE_CF, statusIdsToDelete); } @Override public List getMentionline(String login, int size, String start, String finish) { return getLineFromCF(ColumnFamilyKeys.MENTIONLINE_CF, login, size, start, finish); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraRegistrationRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import com.google.common.collect.Maps; import fr.ippon.tatami.repository.RegistrationRepository; import fr.ippon.tatami.service.util.RandomUtil; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.ColumnQuery; import me.prettyprint.hector.api.query.SliceQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.List; import java.util.Map; import static fr.ippon.tatami.config.ColumnFamilyKeys.REGISTRATION_CF; /** * Cassandra implementation of the Registration repository. *

    * Structure : * - Key = "registration_key" * - Name = key * - Value = login * * @author Julien Dubois */ @Repository public class CassandraRegistrationRepository implements RegistrationRepository { private static final Logger log = LoggerFactory.getLogger(CassandraRegistrationRepository.class); private final static String ROW_KEY = "registration_key"; private final static int COLUMN_TTL = 60 * 60 * 24 * 2; // The column is stored for 2 days. @Inject private Keyspace keyspaceOperator; @Override public String generateRegistrationKey(String login) { String key = RandomUtil.generateRegistrationKey(); HColumn column = HFactory.createColumn(key, login, COLUMN_TTL, StringSerializer.get(), StringSerializer.get()); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(ROW_KEY, REGISTRATION_CF, column); return key; } @Override public String getLoginByRegistrationKey(String registrationKey) { ColumnQuery query = HFactory.createStringColumnQuery(keyspaceOperator); HColumn column = query.setColumnFamily(REGISTRATION_CF) .setKey(ROW_KEY) .setName(registrationKey) .execute() .get(); if (column != null) { return column.getValue(); } else { return null; } } /** * !! For testing purpose only !! * This method is not efficient and is limited to 10000 registrations. * Other limitation : if a login is associated to multiple registrationKey */ public Map _getAllRegistrationKeyByLogin() { log.warn("Calling _getAllRegistrationKeyByLogin() is only for testing purposes!"); Map registrationKeyByLogin = Maps.newHashMap(); SliceQuery sliceQuery = HFactory.createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()); ColumnSlice columnSlice = sliceQuery.setColumnFamily(REGISTRATION_CF) .setKey(ROW_KEY) .setRange(null, null, false, 10000) .execute().get(); List> columns = columnSlice.getColumns(); for (HColumn hColumn : columns) { // WARN : here we don't handle multiple registrationKey for one login registrationKeyByLogin.put(hColumn.getValue(), hColumn.getName()); log.debug("Key={}|Value={}", hColumn.getValue(), hColumn.getName()); } return registrationKeyByLogin; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraRssUidRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.RssUidRepository; import fr.ippon.tatami.service.util.RandomUtil; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.ColumnQuery; import org.springframework.stereotype.Repository; import javax.inject.Inject; import static fr.ippon.tatami.config.ColumnFamilyKeys.RSS_CF; /** * Cassandra implementation of the RssUid repository. *

    * Structure : - Key = "rss_uid" - Name = key - Value = login * * @author Pierre Rust */ @Repository public class CassandraRssUidRepository implements RssUidRepository { private final static String ROW_KEY = "rss_uid"; @Inject private Keyspace keyspaceOperator; @Override public String generateRssUid(String login) { String key = RandomUtil.generateRegistrationKey(); HColumn column = HFactory.createColumn(key, login, StringSerializer.get(), StringSerializer.get()); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(ROW_KEY, RSS_CF, column); return key; } @Override public String getLoginByRssUid(String rssUid) { ColumnQuery query = HFactory.createStringColumnQuery(keyspaceOperator); HColumn column = query.setColumnFamily(RSS_CF) .setKey(ROW_KEY) .setName(rssUid) .execute() .get(); if (column != null) { return column.getValue(); } else { return null; } } @Override public void removeRssUid(String rssUid) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(ROW_KEY, RSS_CF, rssUid, StringSerializer.get()); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraSharesRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.SharesRepository; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.Calendar; import java.util.Collection; import java.util.LinkedHashSet; import static fr.ippon.tatami.config.ColumnFamilyKeys.SHARES_CF; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the Shares repository. * Lists the shares for a given status. *

    * Structure : * - Key = status Id * - Name = time * - Value = login who shared this status * * @author Julien Dubois */ @Repository public class CassandraSharesRepository implements SharesRepository { @Inject private Keyspace keyspaceOperator; @Override @CacheEvict(value = "shared-cache", key = "#statusId") public void newShareByLogin(String statusId, String sharedByLogin) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(statusId, SHARES_CF, HFactory.createColumn( Calendar.getInstance().getTimeInMillis(), sharedByLogin, LongSerializer.get(), StringSerializer.get())); } @Override @Cacheable("shared-cache") public Collection findLoginsWhoSharedAStatus(String statusId) { ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), LongSerializer.get(), StringSerializer.get()) .setColumnFamily(SHARES_CF) .setKey(statusId) .setRange(null, null, false, 100) // Limit to 100 logins .execute() .get(); Collection sharedByLogins = new LinkedHashSet(); for (HColumn column : result.getColumns()) { sharedByLogins.add(column.getValue()); } return sharedByLogins; } @Override public boolean hasBeenShared(String statusId) { int zeroOrOne = HFactory.createCountQuery(keyspaceOperator, StringSerializer.get(), LongSerializer.get()) .setColumnFamily(SHARES_CF) .setKey(statusId) .setRange(null, null, 1) .execute() .get(); return zeroOrOne > 0; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraStatusAttachmentRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.repository.StatusAttachmentRepository; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.serializers.UUIDSerializer; import me.prettyprint.cassandra.service.template.ColumnFamilyResult; import me.prettyprint.cassandra.service.template.ColumnFamilyTemplate; import me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.UUID; import static fr.ippon.tatami.config.ColumnFamilyKeys.STATUS_ATTACHMENT_CF; /** * Cassandra implementation of the StatusAttachmentRepository repository. *

    * Structure : * - Key = statusId * - Name = attachmentId * - Value = time * * @author Julien Dubois */ @Repository public class CassandraStatusAttachmentRepository implements StatusAttachmentRepository { private ColumnFamilyTemplate attachmentsTemplate; @Inject private Keyspace keyspaceOperator; @PostConstruct public void init() { attachmentsTemplate = new ThriftColumnFamilyTemplate(keyspaceOperator, STATUS_ATTACHMENT_CF, StringSerializer.get(), UUIDSerializer.get()); attachmentsTemplate.setCount(Constants.CASSANDRA_MAX_COLUMNS); } @Override public void addAttachmentId(String statusId, String attachmentId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(statusId, STATUS_ATTACHMENT_CF, HFactory.createColumn(UUID.fromString(attachmentId), Calendar.getInstance().getTimeInMillis(), UUIDSerializer.get(), LongSerializer.get())); } @Override public void removeAttachmentId(String statusId, String attachmentId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(statusId, STATUS_ATTACHMENT_CF, UUID.fromString(attachmentId), UUIDSerializer.get()); } @Override public Collection findAttachmentIds(String statusId) { ColumnFamilyResult result = attachmentsTemplate.queryColumns(statusId); Collection attachmentIds = new ArrayList(); for (UUID columnName : result.getColumnNames()) { attachmentIds.add(columnName.toString()); } return attachmentIds; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraStatusReportRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.config.GroupRoles; import fr.ippon.tatami.repository.StatusReportRepository; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.service.ColumnSliceIterator; import me.prettyprint.cassandra.service.template.ColumnFamilyResult; import me.prettyprint.cassandra.service.template.ColumnFamilyTemplate; import me.prettyprint.cassandra.service.template.ColumnFamilyUpdater; import me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.exceptions.HectorException; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.SliceQuery; import org.springframework.stereotype.Repository; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.*; import static fr.ippon.tatami.config.ColumnFamilyKeys.GROUP_MEMBERS_CF; import static fr.ippon.tatami.config.ColumnFamilyKeys.STATUS_REPORT_CF; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; @Repository public class CassandraStatusReportRepository implements StatusReportRepository { private ColumnFamilyTemplate reportedStatusTemplate; @Inject private Keyspace keyspaceOperator; @PostConstruct public void init() { reportedStatusTemplate = new ThriftColumnFamilyTemplate(keyspaceOperator, STATUS_REPORT_CF, StringSerializer.get(), StringSerializer.get()); reportedStatusTemplate.setCount(Constants.CASSANDRA_MAX_COLUMNS); } @Override public void reportStatus(String domain, String reportedStatusId, String reportingLogin) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(domain, STATUS_REPORT_CF, HFactory.createStringColumn(reportedStatusId, reportingLogin)); } @Override public void unreportStatus(String domain, String reportedStatusId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(domain, STATUS_REPORT_CF, reportedStatusId, StringSerializer.get()); } @Override public List findReportedStatuses(String domain) { SliceQuery query = HFactory.createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()). setKey(domain).setColumnFamily(STATUS_REPORT_CF); ColumnSliceIterator iterator = new ColumnSliceIterator(query, null, "\uFFFF", false); List reportedStatuses = new ArrayList(); while (iterator.hasNext()) { reportedStatuses.add(iterator.next().getName()); } return reportedStatuses; } public String findUserHavingReported(String domain, String statusId){ ColumnFamilyResult res = reportedStatusTemplate.queryColumns(domain); return res.getString(statusId); } @Override public boolean hasBeenReportedByUser(String domain, String reportedStatusId, String login) { return login.equals(reportedStatusTemplate.queryColumns(domain).getString(reportedStatusId)); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraStatusRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.Attachment; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.repository.*; import fr.ippon.tatami.service.util.DomainUtil; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.service.template.ColumnFamilyResult; import me.prettyprint.cassandra.service.template.ColumnFamilyTemplate; import me.prettyprint.cassandra.service.template.ColumnFamilyUpdater; import me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate; import me.prettyprint.cassandra.utils.TimeUUIDUtils; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import fr.ippon.tatami.domain.status.*; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.validation.*; import java.util.*; /** * Cassandra implementation of the status repository. *

    * Timeline and Userline have the same structure : * - Key : login * - Name : status Id * - Value : "" * * @author Julien Dubois */ @Repository public class CassandraStatusRepository implements StatusRepository { private final Logger log = LoggerFactory.getLogger(CassandraStatusRepository.class); private static final String LOGIN = "login"; private static final String TYPE = "type"; private static final String USERNAME = "username"; private static final String DOMAIN = "domain"; private static final String STATUS_DATE = "statusDate"; //Normal status private static final String STATUS_PRIVATE = "statusPrivate"; private static final String GROUP_ID = "groupId"; private static final String HAS_ATTACHMENTS = "hasAttachments"; private static final String CONTENT = "content"; private static final String DISCUSSION_ID = "discussionId"; private static final String REPLY_TO = "replyTo"; private static final String REPLY_TO_USERNAME = "replyToUsername"; private static final String REMOVED = "removed"; private static final String GEO_LOCALIZATION = "geoLocalization"; //Share, Mention Share & Announcement private static final String ORIGINAL_STATUS_ID = "originalStatusId"; //Mention Friend private static final String FOLLOWER_LOGIN = "followerLogin"; //Bean validation private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); private static final Validator validator = factory.getValidator(); //Cassandra Template ColumnFamilyTemplate template; @Inject private Keyspace keyspaceOperator; @Inject private DiscussionRepository discussionRepository; @Inject private SharesRepository sharesRepository; @Inject private StatusAttachmentRepository statusAttachmentRepository; @Inject private AttachmentRepository attachmentRepository; @PostConstruct public void init() { template = new ThriftColumnFamilyTemplate( keyspaceOperator, ColumnFamilyKeys.STATUS_CF, StringSerializer.get(), StringSerializer.get()); } @Override public Status createStatus(String login, boolean statusPrivate, Group group, Collection attachmentIds, String content, String discussionId, String replyTo, String replyToUsername, String geoLocalization) throws ConstraintViolationException { Status status = new Status(); status.setLogin(login); status.setType(StatusType.STATUS); String username = DomainUtil.getUsernameFromLogin(login); status.setUsername(username); String domain = DomainUtil.getDomainFromLogin(login); status.setDomain(domain); status.setContent(content); Set> constraintViolations = validator.validate(status); if (!constraintViolations.isEmpty()) { if (log.isDebugEnabled()) { for (ConstraintViolation cv : constraintViolations) { log.debug("Constraint violation: {}", cv.getMessage()); } } throw new ConstraintViolationException(new HashSet>(constraintViolations)); } ColumnFamilyUpdater updater = this.createBaseStatus(status); updater.setString(CONTENT, content); status.setStatusPrivate(statusPrivate); updater.setBoolean(STATUS_PRIVATE, statusPrivate); if (group != null) { String groupId = group.getGroupId(); status.setGroupId(groupId); updater.setString(GROUP_ID, groupId); } if (attachmentIds != null && attachmentIds.size() > 0) { status.setHasAttachments(true); updater.setBoolean(HAS_ATTACHMENTS, true); } if (discussionId != null) { status.setDiscussionId(discussionId); updater.setString(DISCUSSION_ID, discussionId); } if (replyTo != null) { status.setReplyTo(replyTo); updater.setString(REPLY_TO, replyTo); } if (replyToUsername != null) { status.setReplyToUsername(replyToUsername); updater.setString(REPLY_TO_USERNAME, replyToUsername); } if(geoLocalization!=null) { status.setGeoLocalization(geoLocalization); updater.setString(GEO_LOCALIZATION, geoLocalization); } log.debug("Persisting Status : {}", status); template.update(updater); return status; } @Override public Share createShare(String login, String originalStatusId) { Share share = new Share(); share.setLogin(login); share.setType(StatusType.SHARE); String username = DomainUtil.getUsernameFromLogin(login); share.setUsername(username); String domain = DomainUtil.getDomainFromLogin(login); share.setDomain(domain); ColumnFamilyUpdater updater = this.createBaseStatus(share); updater.setString(ORIGINAL_STATUS_ID, originalStatusId); share.setOriginalStatusId(originalStatusId); log.debug("Persisting Share : {}", share); template.update(updater); return share; } @Override public Announcement createAnnouncement(String login, String originalStatusId) { Announcement announcement = new Announcement(); announcement.setLogin(login); announcement.setType(StatusType.ANNOUNCEMENT); String username = DomainUtil.getUsernameFromLogin(login); announcement.setUsername(username); String domain = DomainUtil.getDomainFromLogin(login); announcement.setDomain(domain); ColumnFamilyUpdater updater = this.createBaseStatus(announcement); updater.setString(ORIGINAL_STATUS_ID, originalStatusId); announcement.setOriginalStatusId(originalStatusId); log.debug("Persisting Announcement : {}", announcement); template.update(updater); return announcement; } @Override public MentionFriend createMentionFriend(String login, String followerLogin) { MentionFriend mentionFriend = new MentionFriend(); mentionFriend.setLogin(login); mentionFriend.setType(StatusType.MENTION_FRIEND); String username = DomainUtil.getUsernameFromLogin(login); mentionFriend.setUsername(username); String domain = DomainUtil.getDomainFromLogin(login); mentionFriend.setDomain(domain); ColumnFamilyUpdater updater = this.createBaseStatus(mentionFriend); updater.setString(FOLLOWER_LOGIN, followerLogin); log.debug("Persisting MentionFriend : {}", mentionFriend); template.update(updater); return mentionFriend; } @Override public MentionShare createMentionShare(String login, String originalStatusId) { MentionShare mentionShare = new MentionShare(); mentionShare.setLogin(login); mentionShare.setType(StatusType.MENTION_SHARE); String username = DomainUtil.getUsernameFromLogin(login); mentionShare.setUsername(username); String domain = DomainUtil.getDomainFromLogin(login); mentionShare.setDomain(domain); ColumnFamilyUpdater updater = this.createBaseStatus(mentionShare); updater.setString(ORIGINAL_STATUS_ID, originalStatusId); mentionShare.setOriginalStatusId(originalStatusId); log.debug("Persisting MentionShare : {}", mentionShare); template.update(updater); return mentionShare; } private ColumnFamilyUpdater createBaseStatus(AbstractStatus abstractStatus) { // Generate statusId and statusDate for all statuses String statusId = TimeUUIDUtils.getUniqueTimeUUIDinMillis().toString(); abstractStatus.setStatusId(statusId); ColumnFamilyUpdater updater = template.createUpdater(statusId); Date statusDate = Calendar.getInstance().getTime(); updater.setDate(STATUS_DATE, statusDate); abstractStatus.setStatusDate(statusDate); // Persist common data : login, username, domain, type String login = abstractStatus.getLogin(); if (login == null) { throw new IllegalStateException("Login cannot be null for status: " + abstractStatus); } updater.setString(LOGIN, login); String username = abstractStatus.getUsername(); if (username == null) { throw new IllegalStateException("Username cannot be null for status: " + abstractStatus); } updater.setString(USERNAME, username); String domain = abstractStatus.getDomain(); if (domain == null) { throw new IllegalStateException("Domain cannot be null for status: " + abstractStatus); } updater.setString(DOMAIN, domain); updater.setString(TYPE, abstractStatus.getType().name()); return updater; } @Override @Cacheable("status-cache") public AbstractStatus findStatusById(String statusId) { if (statusId == null || statusId.equals("")) { return null; } if (log.isTraceEnabled()) { log.trace("Finding status : " + statusId); } ColumnFamilyResult result = template.queryColumns(statusId); if (result.hasResults() == false) { return null; // No status was found } AbstractStatus status = null; String type = result.getString(TYPE); if (type == null || type.equals(StatusType.STATUS.name())) { status = findStatus(result, statusId); } else if (type.equals(StatusType.SHARE.name())) { status = findShare(result); } else if (type.equals(StatusType.ANNOUNCEMENT.name())) { status = findAnnouncement(result); } else if (type.equals(StatusType.MENTION_FRIEND.name())) { status = findMentionFriend(result); } else if (type.equals(StatusType.MENTION_SHARE.name())) { status = findMentionShare(result); } else { throw new IllegalStateException("Status has an unknown type: " + type); } if (status == null) { // Status was not found, or was removed return null; } status.setStatusId(statusId); status.setLogin(result.getString(LOGIN)); status.setUsername(result.getString(USERNAME)); String domain = result.getString(DOMAIN); if (domain != null) { status.setDomain(domain); } else { throw new IllegalStateException("Status cannot have a null domain: " + status); } status.setStatusDate(result.getDate(STATUS_DATE)); Boolean removed = result.getBoolean(REMOVED); if (removed != null) { status.setRemoved(removed); } return status; } private Status findStatus(ColumnFamilyResult result, String statusId) { Status status = new Status(); status.setStatusId(statusId); status.setType(StatusType.STATUS); status.setContent(result.getString(CONTENT)); status.setStatusPrivate(result.getBoolean(STATUS_PRIVATE)); status.setGroupId(result.getString(GROUP_ID)); status.setHasAttachments(result.getBoolean(HAS_ATTACHMENTS)); status.setDiscussionId(result.getString(DISCUSSION_ID)); status.setReplyTo(result.getString(REPLY_TO)); status.setReplyToUsername(result.getString(REPLY_TO_USERNAME)); status.setGeoLocalization(result.getString(GEO_LOCALIZATION)); status.setRemoved(result.getBoolean(REMOVED)); if (status.getRemoved() == Boolean.TRUE) { return null; } status.setDetailsAvailable(computeDetailsAvailable(status)); if (status.getHasAttachments() != null && status.getHasAttachments()) { Collection attachmentIds = statusAttachmentRepository.findAttachmentIds(statusId); Collection attachments = new ArrayList(); for (String attachmentId : attachmentIds) { Attachment attachment = attachmentRepository.findAttachmentMetadataById(attachmentId); if (attachment != null) { // We copy everything excepted the attachment content, as we do not want it in the status cache Attachment attachmentCopy = new Attachment(); attachmentCopy.setAttachmentId(attachmentId); attachmentCopy.setSize(attachment.getSize()); attachmentCopy.setFilename(attachment.getFilename()); attachments.add(attachment); } } status.setAttachments(attachments); } return status; } private Share findShare(ColumnFamilyResult result) { Share share = new Share(); share.setType(StatusType.SHARE); share.setOriginalStatusId(result.getString(ORIGINAL_STATUS_ID)); return share; } private Announcement findAnnouncement(ColumnFamilyResult result) { Announcement announcement = new Announcement(); announcement.setType(StatusType.ANNOUNCEMENT); announcement.setOriginalStatusId(result.getString(ORIGINAL_STATUS_ID)); return announcement; } private MentionFriend findMentionFriend(ColumnFamilyResult result) { MentionFriend mentionFriend = new MentionFriend(); mentionFriend.setType(StatusType.MENTION_FRIEND); mentionFriend.setFollowerLogin(result.getString(FOLLOWER_LOGIN)); return mentionFriend; } private MentionShare findMentionShare(ColumnFamilyResult result) { MentionShare mentionShare = new MentionShare(); mentionShare.setType(StatusType.MENTION_SHARE); mentionShare.setOriginalStatusId(result.getString(ORIGINAL_STATUS_ID)); return mentionShare; } @Override @CacheEvict(value = "status-cache", key = "#status.statusId") public void removeStatus(AbstractStatus status) { log.debug("Removing Status : {}", status); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addDeletion(status.getStatusId(), ColumnFamilyKeys.STATUS_CF); mutator.execute(); } private boolean computeDetailsAvailable(Status status) { boolean detailsAvailable = false; if (status.getType().equals(StatusType.STATUS)) { if (StringUtils.isNotBlank(status.getReplyTo())) { detailsAvailable = true; } else if (discussionRepository.hasReply(status.getStatusId())) { detailsAvailable = true; } else if (sharesRepository.hasBeenShared(status.getStatusId())) { detailsAvailable = true; } } return detailsAvailable; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraTagCounterRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.TagCounterRepository; import me.prettyprint.cassandra.model.thrift.ThriftCounterColumnQuery; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.CounterQuery; import org.springframework.stereotype.Repository; import javax.inject.Inject; import static fr.ippon.tatami.config.ColumnFamilyKeys.TAG_COUNTER_CF; /** * Cassandra implementation of the Tag Counter repository. *

    * Structure : * - Key = tag + domain * - Name = TAG_COUNTER * - Value = count * * @author Julien Dubois */ @Repository public class CassandraTagCounterRepository implements TagCounterRepository { private static final String TAG_COUNTER = "TAG_COUNTER"; @Inject private Keyspace keyspaceOperator; @Override public long getTagCounter(String domain, String tag) { CounterQuery counter = new ThriftCounterColumnQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get()); counter.setColumnFamily(TAG_COUNTER_CF).setKey(getKey(domain, tag)).setName(TAG_COUNTER); return counter.execute().get().getValue(); } @Override public void incrementTagCounter(String domain, String tag) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.incrementCounter(getKey(domain, tag), TAG_COUNTER_CF, TAG_COUNTER, 1); } @Override public void decrementTagCounter(String domain, String tag) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.decrementCounter(getKey(domain, tag), TAG_COUNTER_CF, TAG_COUNTER, 1); } @Override public void deleteTagCounter(String domain, String tag) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addCounterDeletion(getKey(domain, tag), TAG_COUNTER_CF, TAG_COUNTER, StringSerializer.get()); mutator.execute(); } /** * Generates the key for this column family. */ private String getKey(String domain, String tag) { return tag.toLowerCase() + "-" + domain; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraTagFollowerRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.TagFollowerRepository; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import java.util.Collection; /** * Cassandra implementation of the Follower repository. *

    * Structure : * - Key = tag + domain * - Name = follower login * - Value = time * * @author Julien Dubois */ @Repository public class CassandraTagFollowerRepository extends AbstractCassandraFollowerRepository implements TagFollowerRepository { @Override public void addFollower(String domain, String tag, String login) { super.addFollower(getKey(domain, tag), login); } @Override public void removeFollower(String domain, String tag, String login) { super.removeFollower(getKey(domain, tag), login); } @Override public Collection findFollowers(String domain, String tag) { return super.findFollowers(getKey(domain, tag)); } @Override public String getFollowersCF() { return ColumnFamilyKeys.TAG_FOLLOWERS_CF; } /** * Generates the key for this column family. */ private String getKey(String domain, String tag) { return tag.toLowerCase() + "-" + domain; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraTaglineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.status.Status; import fr.ippon.tatami.repository.TaglineRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.serializers.UUIDSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.Collection; import java.util.List; import java.util.UUID; import static fr.ippon.tatami.config.ColumnFamilyKeys.TAGLINE_CF; /** * Cassandra implementation of the Tag line repository. *

    * Structure : * - Key = tag + domain * - Name = statusId * - Value = "" * * @author Julien Dubois */ @Repository public class CassandraTaglineRepository extends AbstractCassandraLineRepository implements TaglineRepository { @Inject private Keyspace keyspaceOperator; @Override public void addStatusToTagline(String tag, Status status) { addStatus(getKey(status.getDomain(), tag), TAGLINE_CF, status.getStatusId()); } @Override public void removeStatusesFromTagline(String tag, String domain, Collection statusIdsToDelete) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); for (String statusId : statusIdsToDelete) { mutator.addDeletion( getKey(domain, tag), TAGLINE_CF, UUID.fromString(statusId), UUIDSerializer.get()); } mutator.execute(); } @Override public List getTagline(String domain, String tag, int size, String start, String finish) { return getLineFromCF(TAGLINE_CF, getKey(domain, tag), size, start, finish); } /** * Generates the key for this column family. */ private String getKey(String domain, String tag) { return tag.toLowerCase() + "-" + domain; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraTimelineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.status.Announcement; import fr.ippon.tatami.domain.status.Share; import fr.ippon.tatami.repository.TimelineRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.serializers.UUIDSerializer; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hector.api.query.QueryResult; import org.springframework.stereotype.Repository; import java.util.Collection; import java.util.List; import java.util.UUID; import static fr.ippon.tatami.config.ColumnFamilyKeys.TIMELINE_CF; import static fr.ippon.tatami.config.ColumnFamilyKeys.TIMELINE_SHARES_CF; /** * Cassandra implementation of the Timeline repository. *

    * Structure : * - Key : login * - Name : status Id * - Value : "" * * @author Julien Dubois */ @Repository public class CassandraTimelineRepository extends AbstractCassandraLineRepository implements TimelineRepository { @Override public boolean isStatusInTimeline(String login, String statusId) { QueryResult> isStatusAlreadyinTimeline = findByLoginAndStatusId(TIMELINE_CF, login, UUID.fromString(statusId)); return isStatusAlreadyinTimeline.get() != null; } @Override public void addStatusToTimeline(String login, String statusId) { addStatus(login, TIMELINE_CF, statusId); } @Override public void removeStatusesFromTimeline(String login, Collection statusIdsToDelete) { removeStatuses(login, TIMELINE_CF, statusIdsToDelete); } @Override public void shareStatusToTimeline(String sharedByLogin, String timelineLogin, Share share) { shareStatus(timelineLogin, share, TIMELINE_CF, TIMELINE_SHARES_CF); } @Override public void announceStatusToTimeline(String announcedByLogin, List logins, Announcement announcement) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); for (String login : logins) { mutator.addInsertion(login, TIMELINE_CF, HFactory.createColumn(UUID.fromString(announcement.getStatusId()), "", UUIDSerializer.get(), StringSerializer.get())); } mutator.execute(); } @Override public List getTimeline(String login, int size, String start, String finish) { return getLineFromCF(TIMELINE_CF, login, size, start, finish); } @Override public void deleteTimeline(String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addDeletion(login, TIMELINE_CF); mutator.execute(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraTrendRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.TrendRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.serializers.UUIDSerializer; import me.prettyprint.cassandra.utils.TimeUUIDUtils; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import org.springframework.util.Assert; import javax.inject.Inject; import java.util.*; import static fr.ippon.tatami.config.ColumnFamilyKeys.TRENDS_CF; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the Trends repository. *

    * Structure : * - Key = domain * - Name = date * - Value = tag * * @author Julien Dubois */ @Repository public class CassandraTrendRepository implements TrendRepository { private final static int COLUMN_TTL = 60 * 60 * 24 * 30; // The column is stored for 30 days. private final static int TRENDS_NUMBER_OF_TAGS = 100; @Inject private Keyspace keyspaceOperator; @Override @CacheEvict(value = "domain-tags-cache", key = "#domain") public void addTag(String domain, String tag) { HColumn column = HFactory.createColumn( TimeUUIDUtils.getUniqueTimeUUIDinMillis(), tag, COLUMN_TTL, UUIDSerializer.get(), StringSerializer.get()); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(domain, TRENDS_CF, column); } @Override public List getRecentTags(String domain) { return getRecentTags(domain, TRENDS_NUMBER_OF_TAGS); } @Override public List getRecentTags(String domain, int maxNumber) { ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()) .setColumnFamily(TRENDS_CF) .setKey(domain) .setRange(null, null, true, maxNumber) .execute() .get(); List result = new ArrayList(); String tag; for (HColumn column : query.getColumns()) { tag = column.getValue(); result.add(tag); } return result; } @Cacheable(value = "domain-tags-cache", key = "#domain") public Collection getDomainTags(String domain) { Assert.hasLength(domain); final ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()) .setColumnFamily(TRENDS_CF) .setKey(domain) .setRange(null, null, true, TRENDS_NUMBER_OF_TAGS) .execute() .get(); final Map result = new HashMap(); String tag; for (HColumn column : query.getColumns()) { tag = column.getValue(); result.put(tag.toLowerCase(), tag); } return result.values(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraUserAttachmentRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.repository.UserAttachmentRepository; import me.prettyprint.cassandra.serializers.LongSerializer; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.serializers.UUIDSerializer; import me.prettyprint.cassandra.service.template.ColumnFamilyResult; import me.prettyprint.cassandra.service.template.ColumnFamilyTemplate; import me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.*; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the UserAttachment repository. *

    * Structure : * - Key = login * - Name = attachmentId * - Value = time * * @author Julien Dubois */ @Repository public class CassandraUserAttachmentRepository implements UserAttachmentRepository { private ColumnFamilyTemplate attachmentsTemplate; @Inject private Keyspace keyspaceOperator; @PostConstruct public void init() { attachmentsTemplate = new ThriftColumnFamilyTemplate(keyspaceOperator, ColumnFamilyKeys.USER_ATTACHMENT_CF, StringSerializer.get(), UUIDSerializer.get()); attachmentsTemplate.setCount(Constants.CASSANDRA_MAX_COLUMNS); } @Override public void addAttachmentId(String login, String attachmentId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(login, ColumnFamilyKeys.USER_ATTACHMENT_CF, HFactory.createColumn(UUID.fromString(attachmentId), Calendar.getInstance().getTimeInMillis(), UUIDSerializer.get(), LongSerializer.get())); } @Override public void removeAttachmentId(String login, String attachmentId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(login, ColumnFamilyKeys.USER_ATTACHMENT_CF, UUID.fromString(attachmentId), UUIDSerializer.get()); } @Override public Collection findAttachmentIds(String login, int pagination, String finish) { List> result; if (finish != null) { ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), LongSerializer.get()) .setColumnFamily(ColumnFamilyKeys.USER_ATTACHMENT_CF) .setKey(login) .setRange(UUID.fromString(finish), null, true, pagination) .execute() .get(); result = query.getColumns(); } else { ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), LongSerializer.get()) .setColumnFamily(ColumnFamilyKeys.USER_ATTACHMENT_CF) .setKey(login) .setRange(null, null, true, pagination) .execute() .get(); result = query.getColumns(); } Collection attachmentIds = new ArrayList(); int index = 0; for (HColumn column : result) { attachmentIds.add(column.getName().toString()); index++; } return attachmentIds; } @Override public Collection findAttachmentIds(String login) { ColumnFamilyResult result = attachmentsTemplate.queryColumns(login); Collection attachmentIds = new ArrayList(); for (UUID columnName : result.getColumnNames()) { attachmentIds.add(columnName.toString()); } return attachmentIds; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraUserGroupRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.config.GroupRoles; import fr.ippon.tatami.repository.UserGroupRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.List; import static fr.ippon.tatami.config.ColumnFamilyKeys.USER_GROUPS_CF; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the User groups repository. *

    * Structure : * - Key = login * - Name = group ID * - Value = role * * @author Julien Dubois */ @Repository public class CassandraUserGroupRepository implements UserGroupRepository { @Inject private Keyspace keyspaceOperator; @Override public void addGroupAsMember(String login, String groupId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(login, USER_GROUPS_CF, HFactory.createColumn(groupId, GroupRoles.MEMBER, StringSerializer.get(), StringSerializer.get())); } @Override public void addGroupAsAdmin(String login, String groupId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(login, USER_GROUPS_CF, HFactory.createColumn(groupId, GroupRoles.ADMIN, StringSerializer.get(), StringSerializer.get())); } @Override public void removeGroup(String login, String groupId) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.delete(login, USER_GROUPS_CF, groupId, StringSerializer.get()); } @Override public List findGroups(String login) { List groups = new ArrayList(); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(USER_GROUPS_CF) .setKey(login) .setRange(null, null, false, Integer.MAX_VALUE) .execute() .get(); for (HColumn column : result.getColumns()) { groups.add(column.getName()); } return groups; } @Override public Collection findGroupsAsAdmin(String login) { List groups = new ArrayList(); ColumnSlice result = createSliceQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(USER_GROUPS_CF) .setKey(login) .setRange(null, null, false, Integer.MAX_VALUE) .execute() .get(); for (HColumn column : result.getColumns()) { if (column.getValue() != null && column.getValue().equals(GroupRoles.ADMIN)) { groups.add(column.getName()); } } return groups; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraUserRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.validation.ContraintsUserCreation; import fr.ippon.tatami.repository.CounterRepository; import fr.ippon.tatami.repository.UserRepository; import fr.ippon.tatami.security.AuthenticationService; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import me.prettyprint.hom.EntityManagerImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import javax.inject.Inject; import javax.validation.*; import java.util.HashSet; import java.util.Set; /** * Cassandra implementation of the user repository. * * @author Julien Dubois */ @Repository public class CassandraUserRepository implements UserRepository { private final Logger log = LoggerFactory.getLogger(CassandraUserRepository.class); @Inject private EntityManagerImpl em; @Inject private Keyspace keyspaceOperator; @Inject private CounterRepository counterRepository; @Inject private AuthenticationService authenticationService; private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); private static final Validator validator = factory.getValidator(); @Override @CacheEvict(value = "user-cache", key = "#user.login") public void createUser(User user) { log.debug("Creating user : {}", user); Set> constraintViolations = validator.validate(user, ContraintsUserCreation.class); if (!constraintViolations.isEmpty()) { throw new ConstraintViolationException(new HashSet>(constraintViolations)); } em.persist(user); } @Override @CacheEvict(value = "user-cache", key = "#user.login", beforeInvocation = true) public void updateUser(User user) throws ConstraintViolationException, IllegalArgumentException { log.debug("Updating user : {}", user); Set> constraintViolations = validator.validate(user); if (!constraintViolations.isEmpty()) { throw new ConstraintViolationException(new HashSet>(constraintViolations)); } em.persist(user); } @Override @CacheEvict(value = "user-cache", key = "#user.login") public void deleteUser(User user) { log.debug("Deleting user : {}", user); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addDeletion(user.getLogin(), ColumnFamilyKeys.USER_CF); mutator.execute(); } @Override @Cacheable("user-cache") public User findUserByLogin(String login) { User user; try { user = em.find(User.class, login); } catch (Exception e) { log.debug("Exception while looking for user {} : {}", login, e.toString()); return null; } if (user != null) { user.setStatusCount(counterRepository.getStatusCounter(login)); user.setFollowersCount(counterRepository.getFollowersCounter(login)); user.setFriendsCount(counterRepository.getFriendsCounter(login)); user.setIsAdmin(authenticationService.isCurrentUserInRole("ROLE_ADMIN")); } return user; } @Override @CacheEvict(value = "user-cache", key = "#user.login") public void desactivateUser( User user ) { user.setActivated(false); em.persist(user); } @Override @CacheEvict(value = "user-cache", key = "#user.login") public void reactivateUser( User user ) { user.setActivated(true); em.persist(user); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraUserTagRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.UserTagRepository; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import java.util.Collection; /** * Cassandra implementation of the TagFriend repository. *

    * Structure : * - Key = login * - Name = tag + domain * - Value = time * * @author Julien Dubois */ @Repository public class CassandraUserTagRepository extends AbstractCassandraFriendRepository implements UserTagRepository { @Override public void addTag(String login, String friendTag) { super.addFriend(login, friendTag); } @Override public void removeTag(String login, String friendTag) { super.removeFriend(login, friendTag); } @Override public Collection findTags(String login) { return super.findFriends(login); } @Override public String getFriendsCF() { return ColumnFamilyKeys.USER_TAGS_CF; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraUserTrendRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.repository.UserTrendRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.serializers.UUIDSerializer; import me.prettyprint.cassandra.utils.TimeUUIDUtils; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import fr.ippon.tatami.config.ColumnFamilyKeys; import javax.inject.Inject; import java.util.*; import static me.prettyprint.hector.api.factory.HFactory.createSliceQuery; /** * Cassandra implementation of the User Trends repository. *

    * Structure : * - Key = login * - Name = date * - Value = tag * * @author Julien Dubois */ @Repository public class CassandraUserTrendRepository implements UserTrendRepository { private final static int COLUMN_TTL = 60 * 60 * 24 * 90; // The column is stored for 90 days. private final static int TRENDS_NUMBER_OF_TAGS = 50; @Inject private Keyspace keyspaceOperator; @Override public void addTag(String login, String tag) { HColumn column = HFactory.createColumn( TimeUUIDUtils.getUniqueTimeUUIDinMillis(), tag, COLUMN_TTL, UUIDSerializer.get(), StringSerializer.get()); Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.insert(login, ColumnFamilyKeys.USER_TRENDS_CF, column); } @Override public List getRecentTags(String login) { ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()) .setColumnFamily(ColumnFamilyKeys.USER_TRENDS_CF) .setKey(login) .setRange(null, null, true, TRENDS_NUMBER_OF_TAGS) .execute() .get(); List result = new ArrayList(); for (HColumn column : query.getColumns()) { String tag = column.getValue(); result.add(tag); } return result; } @Override public Collection getUserRecentTags(String login, Date endDate, int nbRecentTags) { ColumnSlice query = createSliceQuery(keyspaceOperator, StringSerializer.get(), UUIDSerializer.get(), StringSerializer.get()) .setColumnFamily(ColumnFamilyKeys.USER_TRENDS_CF) .setKey(login) .setRange(null, TimeUUIDUtils.getTimeUUID(endDate.getTime()), true, nbRecentTags) .execute() .get(); Map result = new HashMap(); String tag; for (HColumn column : query.getColumns()) { tag = column.getValue(); result.put(tag.toLowerCase(), tag); } return result.values(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/repository/cassandra/CassandraUserlineRepository.java ================================================ package fr.ippon.tatami.repository.cassandra; import fr.ippon.tatami.domain.status.Share; import fr.ippon.tatami.repository.UserlineRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.mutation.Mutator; import org.springframework.stereotype.Repository; import java.util.Collection; import java.util.List; import static fr.ippon.tatami.config.ColumnFamilyKeys.USERLINE_CF; import static fr.ippon.tatami.config.ColumnFamilyKeys.USERLINE_SHARES_CF; /** * Cassandra implementation of the Userline repository. *

    * Structure : * - Key : login * - Name : status Id * - Value : "" * * @author Julien Dubois */ @Repository public class CassandraUserlineRepository extends AbstractCassandraLineRepository implements UserlineRepository { @Override public void addStatusToUserline(String login, String statusId) { addStatus(login,USERLINE_CF, statusId); } @Override public void removeStatusesFromUserline(String login, Collection statusIdsToDelete) { removeStatuses(login, USERLINE_CF, statusIdsToDelete); } @Override public void shareStatusToUserline(String currentLogin, Share share) { shareStatus(currentLogin, share, USERLINE_CF, USERLINE_SHARES_CF); } @Override public List getUserline(String login, int size, String start, String finish) { return getLineFromCF(USERLINE_CF, login, size, start, finish); } @Override public void deleteUserline(String login) { Mutator mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get()); mutator.addDeletion(login, USERLINE_CF); mutator.execute(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/AjaxAuthenticationFailureHandler.java ================================================ package fr.ippon.tatami.security; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Returns a 401 error code (Unauthorized) to the client, when Ajax authentication fails. */ @Component public class AjaxAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed"); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/AjaxAuthenticationSuccessHandler.java ================================================ package fr.ippon.tatami.security; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Spring Security success handler, specialized for Ajax requests. */ @Component public class AjaxAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_OK); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/AjaxLogoutSuccessHandler.java ================================================ package fr.ippon.tatami.security; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AbstractAuthenticationTargetUrlRequestHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Spring Security logout handler, specialized for Ajax requests. */ @Component public class AjaxLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_OK); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/AuthenticationService.java ================================================ package fr.ippon.tatami.security; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.UserRepository; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import javax.inject.Inject; /** * This service is user to find the current user. * * @author Julien Dubois */ @Service public class AuthenticationService { @Inject private UserRepository userRepository; public User getCurrentUser() { SecurityContext securityContext = SecurityContextHolder.getContext(); UserDetails springSecurityUser = (UserDetails) securityContext .getAuthentication().getPrincipal(); return userRepository.findUserByLogin(springSecurityUser.getUsername()); } public static boolean isCurrentUserInRole(String authority) { SecurityContext securityContext = SecurityContextHolder.getContext(); Authentication authentication = securityContext.getAuthentication(); if (authentication != null) { if (authentication.getPrincipal() instanceof UserDetails) { UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal(); return springSecurityUser.getAuthorities().contains(new SimpleGrantedAuthority(authority)); } } return false; } public boolean hasAuthenticatedUser() { SecurityContext securityContext = SecurityContextHolder.getContext(); return (securityContext.getAuthentication() != null); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/DomainViolationException.java ================================================ package fr.ippon.tatami.security; /** * This exception is thrown when a user tries to see a status from another domain. * * @author Julien Dubois */ public class DomainViolationException extends RuntimeException { public DomainViolationException() { } public DomainViolationException(String s) { super(s); } public DomainViolationException(String s, Throwable throwable) { super(s, throwable); } public DomainViolationException(Throwable throwable) { super(throwable); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/GoogleApiAuthenticationProvider.java ================================================ package fr.ippon.tatami.security; import org.pac4j.springframework.security.authentication.ClientAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; public class GoogleApiAuthenticationProvider implements AuthenticationProvider { Logger logger = LoggerFactory.getLogger(GoogleApiAuthenticationProvider.class); @Override public Authentication authenticate(Authentication authentication) { if(!this.supports(authentication.getClass())) { logger.debug("unsupported authentication class : {}", authentication.getClass()); return null; } else { logger.debug("authentication : {}", authentication); GoogleAuthenticationToken result = (GoogleAuthenticationToken) authentication; result.setDetails(authentication.getDetails()); return result; } } @Override public boolean supports(Class authentication) { return GoogleAuthenticationToken.class.isAssignableFrom(authentication); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/GoogleAuthenticationProvider.java ================================================ package fr.ippon.tatami.security; import com.google.inject.Inject; import org.pac4j.core.client.Client; import org.pac4j.core.client.Clients; import org.pac4j.core.context.WebContext; import org.pac4j.core.credentials.Credentials; import org.pac4j.core.profile.UserProfile; import org.pac4j.springframework.security.authentication.ClientAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; /** * */ public class GoogleAuthenticationProvider implements AuthenticationProvider { private static final Logger logger = LoggerFactory.getLogger(GoogleAuthenticationProvider.class); // In our case, the only actual client is the google client @Inject private Clients clients; // Get/Create new user @Inject private AuthenticationUserDetailsService userDetailsService; public GoogleAuthenticationProvider() { } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { logger.debug("authentication : {}", authentication); if(!this.supports(authentication.getClass())) { logger.debug("unsupported authentication class : {}", authentication.getClass()); return null; } else { ClientAuthenticationToken token = (ClientAuthenticationToken)authentication; Credentials credentials = (Credentials)authentication.getCredentials(); logger.debug("credentials : {}", credentials); String clientName = token.getClientName(); Client client = this.clients.findClient(clientName); UserProfile userProfile = client.getUserProfile(credentials, (WebContext)null); logger.debug("userProfile : {}", userProfile); Object authorities = new ArrayList(); ClientAuthenticationToken result = null; logger.debug("userDetailsService: {}", this.userDetailsService); result = new ClientAuthenticationToken(credentials, clientName, userProfile, (Collection)null); UserDetails userDetails = this.userDetailsService.loadUserDetails(result); logger.debug("userDetails : {}", userDetails); if(userDetails != null) { authorities = userDetails.getAuthorities(); logger.debug("authorities : {}", authorities); } GoogleAuthenticationToken res = new GoogleAuthenticationToken(userDetails, clientName, (Collection)authorities); logger.debug("Client name : {}", clientName); // -> Google2Client logger.debug("Client Credentials: {}", credentials); // -> OAuth Credentials logger.debug("Client Profile: {}", userProfile); // -> GoogleProfile, i.e. data from google res.setDetails(authentication.getDetails()); logger.debug("result : {}", res); return res; } } public UserDetails createPrincipal() { return null; } @Override public boolean supports(Class authentication) { return ClientAuthenticationToken.class.isAssignableFrom(authentication); } public void setUserDetailsService(AuthenticationUserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } public AuthenticationUserDetailsService getUserDetailsService() { return this.userDetailsService; } public void setClients(Clients clients) { this.clients = clients; } public Clients getClients() { return clients; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/GoogleAuthenticationToken.java ================================================ package fr.ippon.tatami.security; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; /** * Pac4j uses the ClientAuthenticationToken, where the principal is a string, but * our AuthenticationService expects a UsersDetails object for the principal. This * class handles this, it takes the ClientAuthenticationToken, and makes it fix what * we expect. */ public class GoogleAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; private Object credentials; public GoogleAuthenticationToken(Object principal) { this(principal, null); } public GoogleAuthenticationToken(Object principal, Object credentials) { super((Collection)null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); } public GoogleAuthenticationToken(Object principal, Object credentials, Collection authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if(isAuthenticated) { throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } else { super.setAuthenticated(false); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/GoogleAutoRegisteringUserDetailsService.java ================================================ package fr.ippon.tatami.security; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.DomainRepository; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.util.DomainUtil; import org.pac4j.springframework.security.authentication.ClientAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import javax.inject.Inject; /** * */ @Component public class GoogleAutoRegisteringUserDetailsService implements AuthenticationUserDetailsService { private final Logger log = LoggerFactory.getLogger(GoogleAutoRegisteringUserDetailsService.class); private static final String EMAIL_ATTRIBUTE = "email"; private static final String FIRSTNAME_ATTRIBUTE = "given_name"; private static final String LASTNAME_ATTRIBUTE = "family_name"; private static final String FULLNAME_ATTRIBUTE = "name"; @Inject private UserService userService; @Inject private DomainRepository domainRepository; @Inject private TatamiUserDetailsService userDetailsService; // => handles grantedAuthorities @Override public UserDetails loadUserDetails(ClientAuthenticationToken token) throws UsernameNotFoundException { String login = getAttributeValue(token, EMAIL_ATTRIBUTE); if (login == null) { String msg = "OAuth response did not contain the user email"; log.error(msg); throw new UsernameNotFoundException(msg); } if (!login.contains("@")) { log.debug("User login {} from OAuth response is incorrect.", login); throw new UsernameNotFoundException("OAuth response did not contains a valid user email"); } // Automatically create OpenId users in Tatami : UserDetails userDetails; try { userDetails = userDetailsService.loadUserByUsername(login); // ensure that this user has access to its domain if it has been created before domainRepository.updateUserInDomain(DomainUtil.getDomainFromLogin(login), login); } catch (UsernameNotFoundException e) { log.info("User with login : \"{}\" doesn't exist yet in Tatami database - creating it...", login); userDetails = getNewlyCreatedUserDetails(token); } return userDetails; } private org.springframework.security.core.userdetails.User getNewlyCreatedUserDetails(ClientAuthenticationToken token) { String login = getAttributeValue(token, EMAIL_ATTRIBUTE); String firstName = getAttributeValue(token, FIRSTNAME_ATTRIBUTE); String lastName = getAttributeValue(token, LASTNAME_ATTRIBUTE); String fullName = getAttributeValue(token, FULLNAME_ATTRIBUTE); if (firstName == null && lastName == null) { // if we haven't first nor last name, we use fullName as last name to begin with : lastName = fullName; } User user = new User(); // Note : The email could change... and the OpenId not // moreover an OpenId account could potentially be associated with several email addresses // so we store it for future use case : user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); log.debug("User: {}", user); userService.createUser(user); return userDetailsService.getTatamiUserDetails(login, user.getPassword()); } private String getAttributeValue(ClientAuthenticationToken token, String name) { return (String) token.getUserProfile().getAttributes().get(name); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/Http401UnauthorizedEntryPoint.java ================================================ package fr.ippon.tatami.security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Returns a 401 error code (Unauthorized) to the client. */ @Component public class Http401UnauthorizedEntryPoint implements AuthenticationEntryPoint { private final Logger log = LoggerFactory.getLogger(Http401UnauthorizedEntryPoint.class); /** * Always returns a 401 error code to the client. */ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException, ServletException { log.debug("Pre-authenticated entry point called. Rejecting access"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied"); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/OpenIdAutoRegisteringUserDetailsService.java ================================================ package fr.ippon.tatami.security; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.DomainRepository; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.util.DomainUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.openid.OpenIDAttribute; import org.springframework.security.openid.OpenIDAuthenticationToken; import org.springframework.stereotype.Component; import javax.inject.Inject; import java.util.List; /** * UserDetails Service to be used with OpenId authentication. * It auto-registers the user based on its "email" OpenId attribute * (that must have been asked to the OpenId provider). * * @author Fabien Arrault */ @Component public class OpenIdAutoRegisteringUserDetailsService implements AuthenticationUserDetailsService { private static final String EMAIL_ATTRIBUTE = "email"; private static final String FIRSTNAME_ATTRIBUTE = "firstname"; private static final String LASTNAME_ATTRIBUTE = "lastname"; private static final String FULLNAME_ATTRIBUTE = "fullname"; private final Logger log = LoggerFactory.getLogger(OpenIdAutoRegisteringUserDetailsService.class); @Inject private UserService userService; @Inject private DomainRepository domainRepository; @Inject private TatamiUserDetailsService userDetailsService; // => handles grantedAuthorities @Override public UserDetails loadUserDetails(OpenIDAuthenticationToken token) throws UsernameNotFoundException { String login = getAttributeValue(token, EMAIL_ATTRIBUTE); // Important security assumption : here we are trusting the OpenID provider // to give us an email that has already been verified to belong to the user if (login == null) { String msg = "OpendId response did not contain the user email"; log.error(msg); throw new UsernameNotFoundException(msg); } if (!login.contains("@")) { log.debug("User login {} from OpenId response is incorrect.", login); throw new UsernameNotFoundException("OpendId response did not contains a valid user email"); } // Automatically create OpenId users in Tatami : UserDetails userDetails; try { userDetails = userDetailsService.loadUserByUsername(login); // ensure that this user has access to its domain if it has been created before domainRepository.updateUserInDomain(DomainUtil.getDomainFromLogin(login), login); } catch (UsernameNotFoundException e) { log.info("User with login : \"{}\" doesn't exist yet in Tatami database - creating it...", login); userDetails = getNewlyCreatedUserDetails(token); } return userDetails; } private org.springframework.security.core.userdetails.User getNewlyCreatedUserDetails(OpenIDAuthenticationToken token) { String login = getAttributeValue(token, EMAIL_ATTRIBUTE); String firstName = getAttributeValue(token, FIRSTNAME_ATTRIBUTE); String lastName = getAttributeValue(token, LASTNAME_ATTRIBUTE); String fullName = getAttributeValue(token, FULLNAME_ATTRIBUTE); if (firstName == null && lastName == null) { // if we haven't first nor last name, we use fullName as last name to begin with : lastName = fullName; } User user = new User(); // Note : The email could change... and the OpenId not // moreover an OpenId account could potentially be associated with several email addresses // so we store it for future use case : user.setOpenIdUrl(token.getName()); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); userService.createUser(user); return userDetailsService.getTatamiUserDetails(login, user.getPassword()); } private String getAttributeValue(OpenIDAuthenticationToken token, String name) { String value = null; for (OpenIDAttribute attribute : token.getAttributes()) { if (name.equals(attribute.getName())) { List values = attribute.getValues(); String firstValue = values.isEmpty() ? null : values.iterator().next(); value = firstValue; break; } } return value; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/TatamiAuthenticationSuccessHandler.java ================================================ package fr.ippon.tatami.security; import fr.ippon.tatami.repository.AppleDeviceRepository; import fr.ippon.tatami.repository.AppleDeviceUserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Tatami specific Spring Security success handler, that understands Ajax requests. */ @Component public class TatamiAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final Logger log = LoggerFactory.getLogger(TatamiAuthenticationSuccessHandler.class); private RequestCache requestCache = new HttpSessionRequestCache(); @Inject private AppleDeviceRepository appleDeviceRepository; @Inject private AppleDeviceUserRepository appleDeviceUserRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); manageAppleDevice(authentication.getName(), request.getParameter("device_token")); if (savedRequest == null) { super.onAuthenticationSuccess(request, response, authentication); return; } if (savedRequest.getHeaderNames().contains("X-Requested-With") && "XMLHttpRequest".equals(savedRequest.getHeaderValues("X-Requested-With").get(0))) { requestCache.removeRequest(request, response); clearAuthenticationAttributes(request); response.sendRedirect(getDefaultTargetUrl()); return; } String targetUrlParameter = getTargetUrlParameter(); if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) { requestCache.removeRequest(request, response); super.onAuthenticationSuccess(request, response, authentication); return; } clearAuthenticationAttributes(request); // Use the DefaultSavedRequest URL String targetUrl = savedRequest.getRedirectUrl(); getRedirectStrategy().sendRedirect(request, response, targetUrl); } public void setRequestCache(RequestCache requestCache) { this.requestCache = requestCache; } private void manageAppleDevice(String login, String deviceToken) { if (deviceToken == null) { return; } log.debug("Device token: {}", deviceToken); String deviceId = deviceToken.substring(1, deviceToken.length() - 1); log.debug("Device Id: {}", deviceId); String existingDeviceLogin = appleDeviceUserRepository.findLoginForDeviceId(deviceId); if (existingDeviceLogin != null) { appleDeviceRepository.removeAppleDevice(existingDeviceLogin, deviceId); appleDeviceUserRepository.removeAppleDeviceForUser(deviceId); } appleDeviceUserRepository.createAppleDeviceForUser(deviceId, login); appleDeviceRepository.createAppleDevice(login, deviceId); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/TatamiLdapAuthenticationProvider.java ================================================ package fr.ippon.tatami.security; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.DomainRepository; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.util.DomainUtil; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; import org.springframework.security.ldap.authentication.LdapAuthenticator; import javax.inject.Inject; /** * Tatami specific LdapAuthenticationProvider. * * @author Julien Dubois */ public class TatamiLdapAuthenticationProvider extends LdapAuthenticationProvider { private final Logger log = LoggerFactory.getLogger(TatamiLdapAuthenticationProvider.class); @Inject private UserService userService; @Inject private DomainRepository domainRepository; @Inject private TatamiUserDetailsService userDetailsService; // => handles grantedAuthorities /** * The domain on which this provider is suitable to authenticate user */ private String managedDomain; public TatamiLdapAuthenticationProvider(LdapAuthenticator authenticator, String managedDomain) { super(authenticator); if (StringUtils.isEmpty(managedDomain)) { throw new IllegalArgumentException("You must provide a managedDomain on this TatamiLdapAuthenticationProvider"); } this.managedDomain = managedDomain; } private boolean canHandleAuthentication(Authentication authentication) { String login = authentication.getName(); if (!login.contains("@")) { log.debug("User login {} is incorrect.", login); throw new BadCredentialsException(messages.getMessage( "LdapAuthenticationProvider.badCredentials", "Bad credentials")); } String domain = DomainUtil.getDomainFromLogin(login); return domain.equalsIgnoreCase(managedDomain); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (!canHandleAuthentication(authentication)) { return null; // this provider is not suitable for this domain } log.debug("Authenticating {} with LDAP", authentication.getName()); String login = authentication.getName().toLowerCase(); String username = DomainUtil.getUsernameFromLogin(login); // Use temporary token to use username, and not login to authenticate on ldap : UsernamePasswordAuthenticationToken tmpAuthentication = new UsernamePasswordAuthenticationToken(username, authentication.getCredentials(), null); try { super.authenticate(tmpAuthentication); } catch (InternalAuthenticationServiceException iase) { // Without this : there is no log when the ldap server or the ldap configuration is broken : log.error("Internal Error while authenticating " + authentication.getName() + " with LDAP", iase); throw iase; } //Automatically create LDAP users in Tatami User user = userService.getUserByLogin(login); if (user == null) { user = new User(); user.setLogin(login); userService.createUser(user); } else { // ensure that this user has access to its domain if it has been created before domainRepository.updateUserInDomain(user.getDomain(), user.getLogin()); } // The real authentication object uses the login, and not the username org.springframework.security.core.userdetails.User realUser = userDetailsService.getTatamiUserDetails(login, authentication.getCredentials().toString()); return new UsernamePasswordAuthenticationToken(realUser, authentication.getCredentials(), realUser.getAuthorities()); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/TatamiUserDetailsService.java ================================================ package fr.ippon.tatami.security; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; /** * Finds a user in Cassandra. * * @author Julien Dubois */ @Component("userDetailsService") public class TatamiUserDetailsService implements UserDetailsService { private final Logger log = LoggerFactory.getLogger(TatamiUserDetailsService.class); private final Collection userGrantedAuthorities = new ArrayList(); private final Collection adminGrantedAuthorities = new ArrayList(); private Collection adminUsers = null; @Inject private UserService userService; @Inject Environment env; @PostConstruct public void init() { if (userGrantedAuthorities.size() == 0) { // to prevent a bug that makes this bean initialized twice //Roles for "normal" users GrantedAuthority roleUser = new SimpleGrantedAuthority("ROLE_USER"); userGrantedAuthorities.add(roleUser); //Roles for "admin" users, configured in tatami.properties GrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN"); adminGrantedAuthorities.add(roleUser); adminGrantedAuthorities.add(roleAdmin); String adminUsersList = env.getProperty("tatami.admin.users"); String[] adminUsersArray = adminUsersList.split(","); adminUsers = new ArrayList(Arrays.asList(adminUsersArray)); if (log.isDebugEnabled()) { for (String admin : adminUsers) { log.debug("Initialization : user \"{}\" is an administrator", admin); } } } } @Override public UserDetails loadUserByUsername(final String login) throws UsernameNotFoundException { log.debug("Authenticating {} with Cassandra", login); String lowercaseLogin = login.toLowerCase(); User userFromCassandra = userService.getUserByLogin(lowercaseLogin); if (userFromCassandra == null) { throw new UsernameNotFoundException("User " + lowercaseLogin + " was not found in Cassandra"); } else if ( userFromCassandra.getActivated() != null && userFromCassandra.getActivated() == false ) { throw new UsernameNotFoundException("User " + lowercaseLogin + " is deactivated. Contact administrator for further details." ); } return getTatamiUserDetails(lowercaseLogin, userFromCassandra.getPassword()); } protected org.springframework.security.core.userdetails.User getTatamiUserDetails(String login, String password) { Collection grantedAuthorities; if (adminUsers.contains(login)) { log.debug("User \"{}\" is an administrator", login); grantedAuthorities = adminGrantedAuthorities; } else { grantedAuthorities = userGrantedAuthorities; } return new org.springframework.security.core.userdetails.User(login, password, grantedAuthorities); } public Collection getAdminUsers() { return adminUsers; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/xauth/Token.java ================================================ package fr.ippon.tatami.security.xauth; /** * The security token. */ public class Token { String token; long expires; public Token(String token, long expires){ this.token = token; this.expires = expires; } public String getToken() { return token; } public long getExpires() { return expires; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/xauth/TokenProvider.java ================================================ package fr.ippon.tatami.security.xauth; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.codec.Hex; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class TokenProvider { private final String secretKey; private final int tokenValidity; public TokenProvider(String secretKey, int tokenValidity) { this.secretKey = secretKey; this.tokenValidity = tokenValidity; } public Token createToken(UserDetails userDetails) { long expires = System.currentTimeMillis() + 1000L * tokenValidity; String token = userDetails.getUsername() + ":" + expires + ":" + computeSignature(userDetails, expires); return new Token(token, expires); } public String computeSignature(UserDetails userDetails, long expires) { StringBuilder signatureBuilder = new StringBuilder(); signatureBuilder.append(userDetails.getUsername()).append(":"); signatureBuilder.append(expires).append(":"); signatureBuilder.append(userDetails.getPassword()).append(":"); signatureBuilder.append(secretKey); MessageDigest digest; try { digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("No MD5 algorithm available!"); } return new String(Hex.encode(digest.digest(signatureBuilder.toString().getBytes()))); } public String getUserNameFromToken(String authToken) { if (null == authToken) { return null; } String[] parts = authToken.split(":"); return parts[0]; } public boolean validateToken(String authToken, UserDetails userDetails) { String[] parts = authToken.split(":"); long expires = Long.parseLong(parts[1]); String signature = parts[2]; String signatureToMatch = computeSignature(userDetails, expires); return expires >= System.currentTimeMillis() && constantTimeEquals(signature, signatureToMatch); } /** * String comparison that doesn't stop at the first character that is different but instead always * iterates the whole string length to prevent timing attacks. */ private boolean constantTimeEquals(String a, String b) { if (a.length() != b.length()) { return false; } else { int equal = 0; for (int i = 0; i < a.length(); i++) { equal |= a.charAt(i) ^ b.charAt(i); } return equal == 0; } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/security/xauth/XAuthTokenFilter.java ================================================ package fr.ippon.tatami.security.xauth; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.util.StringUtils; import org.springframework.web.filter.GenericFilterBean; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * Filters incoming requests and installs a Spring Security principal * if a header corresponding to a valid user is found. */ public class XAuthTokenFilter extends GenericFilterBean { public final static String XAUTH_TOKEN_HEADER_NAME = "x-auth-token"; private UserDetailsService detailsService; private TokenProvider tokenProvider; public XAuthTokenFilter(UserDetailsService detailsService, TokenProvider tokenProvider) { this.detailsService = detailsService; this.tokenProvider = tokenProvider; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String authToken = httpServletRequest.getHeader(XAUTH_TOKEN_HEADER_NAME); if (StringUtils.hasText(authToken) && authToken.split(":").length > 1) { String username = this.tokenProvider.getUserNameFromToken(authToken); UserDetails details = this.detailsService.loadUserByUsername(username); if (this.tokenProvider.validateToken(authToken, details)) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(details, details.getPassword(), details.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(token); } } filterChain.doFilter(servletRequest, servletResponse); } catch (Exception ex) { throw new RuntimeException(ex); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/AdminService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.Domain; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.AbstractStatus; import fr.ippon.tatami.domain.status.Status; import fr.ippon.tatami.domain.status.StatusType; import fr.ippon.tatami.repository.DomainRepository; import fr.ippon.tatami.repository.StatusRepository; import fr.ippon.tatami.repository.UserRepository; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.OrderedRows; import me.prettyprint.hector.api.beans.Row; import me.prettyprint.hector.api.query.QueryResult; import me.prettyprint.hector.api.query.RangeSlicesQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.util.*; import static fr.ippon.tatami.config.ColumnFamilyKeys.STATUS_CF; import static me.prettyprint.hector.api.factory.HFactory.createRangeSlicesQuery; /** * Administration service. Only users with the "admin" role should access it. * * @author Julien Dubois */ @Service @PreAuthorize("hasRole('ROLE_ADMIN')") public class AdminService { private static final Logger log = LoggerFactory.getLogger(AdminService.class); @Inject private DomainRepository domainRepository; @Inject private SearchService searchService; @Inject private UserRepository userRepository; @Inject private StatusRepository statusRepository; @Inject private GroupService groupService; @Inject private Environment env; @Inject private Keyspace keyspaceOperator; public Collection getAllDomains() { return domainRepository.getAllDomains(); } public Map getEnvProperties() { log.trace("AdminService.getEnvProperties() enter"); Map properties = new LinkedHashMap(); loadProperty(properties, "tatami.version"); loadProperty(properties, "tatami.wro4j.enabled"); loadProperty(properties, "tatami.google.analytics.key"); loadProperty(properties, "tatami.message.reloading.enabled"); loadProperty(properties, "smtp.host"); loadProperty(properties, "cassandra.host"); loadProperty(properties, "search.engine"); loadProperty(properties, "lucene.path"); loadProperty(properties, "elasticsearch.indexNamePrefix"); loadProperty(properties, "elasticsearch.cluster.name"); loadProperty(properties, "elasticsearch.cluster.nodes"); loadProperty(properties, "elasticsearch.cluster.default.communication.port"); log.trace("AdminService.getEnvProperties() return"); return properties; } private void loadProperty(Map properties, String key) { try { properties.put(key, env.getProperty(key)); } catch (Exception e) { properties.put(key, "(Invalid value)"); } } /** * Rebuilds the Search Engine Index. *

    * This could be a huge batch process : it does not use a Repository for performance reasons. *

    */ public void rebuildIndex() { log.info("Search engine Index rebuild triggered."); log.debug("Deleting Index"); if (searchService.reset()) { log.info("Search engine Index deleted."); } else { log.error("An error has occured while deleting the Search Engine Index. " + "Full rebuild of the index cancelled."); return; } //Rebuild the user Index log.debug("Rebuilding the user & group Indexes"); long fullIndexStartTime = Calendar.getInstance().getTimeInMillis(); Collection domains = domainRepository.getAllDomains(); int groupCount = 0; for (Domain domain : domains) { log.debug("Indexing domain: " + domain.getName()); List logins = domainRepository.getLoginsInDomain(domain.getName()); Collection users = new ArrayList(); for (String login : logins) { User user = userRepository.findUserByLogin(login); if (user == null) { log.warn("User defined in domain was not found in the user respository: " + login); } else { log.debug("Indexing user: {}", login); users.add(user); Collection groups = groupService.getGroupsWhereUserIsAdmin(user); for (Group group : groups) { searchService.addGroup(group); groupCount++; } } } searchService.addUsers(users); log.info("The search engine indexed " + logins.size() + " users."); } log.info("The search engine indexed " + groupCount + " groups."); //Rebuild the status Index log.info("Rebuilding the status Index"); String startKey = null; boolean moreStatus = true; while (moreStatus) { long startTime = Calendar.getInstance().getTimeInMillis(); RangeSlicesQuery query = createRangeSlicesQuery(keyspaceOperator, StringSerializer.get(), StringSerializer.get(), StringSerializer.get()) .setColumnFamily(STATUS_CF) .setRange("statusId", "statusId", false, 1) .setKeys(startKey, null) .setRowCount(1001); QueryResult> result = query.execute(); List> rows = result.get().getList(); if (rows.size() == 1001) { // Calculate the pagination startKey = rows.get(1000).getKey(); rows = rows.subList(0, 1000); } else { moreStatus = false; } Collection statuses = new ArrayList(); for (Row row : rows) { AbstractStatus abstractStatus = statusRepository.findStatusById(row.getKey()); // This makes 2 calls to the same row if (abstractStatus != null && // if a status has been removed, it is returned as null abstractStatus.getType().equals(StatusType.STATUS)) { // Only index standard statuses Status status = (Status) abstractStatus; if (status.getStatusPrivate() == null || !status.getStatusPrivate()) { statuses.add(status); } } } searchService.addStatuses(statuses); // This should be batched for optimum performance log.info("The search engine indexed " + statuses.size() + " statuses in " + (Calendar.getInstance().getTimeInMillis() - startTime) + " ms."); } log.info("Search engine index rebuilt in " + (Calendar.getInstance().getTimeInMillis() - fullIndexStartTime) + " ms."); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/ApplePushService.java ================================================ package fr.ippon.tatami.service; import com.notnoop.apns.APNS; import com.notnoop.apns.ApnsService; import com.notnoop.exceptions.NetworkIOException; import fr.ippon.tatami.domain.status.Status; import fr.ippon.tatami.repository.AppleDeviceRepository; import fr.ippon.tatami.repository.AppleDeviceUserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.Collection; import java.util.Date; import java.util.Map; /** * Notifies users with iOS push notifications. */ @Service @Profile("apple-push") public class ApplePushService { private static final Logger log = LoggerFactory.getLogger(ApplePushService.class); private ApnsService apnsService; @Inject private Environment env; @Inject private AppleDeviceRepository appleDeviceRepository; @Inject private AppleDeviceUserRepository appleDeviceUserRepository; @PostConstruct public void init() { log.info("Creating Apple Push Service"); String certificate = env.getRequiredProperty("apple.push.certificate"); String password = env.getRequiredProperty("apple.push.password"); apnsService = APNS.newService() .withCert(certificate, password) .withSandboxDestination() .build(); try { apnsService.testConnection(); log.info("Apple Push Service is OK"); } catch (NetworkIOException nioe) { log.warn("Apple Push Service is NOT OK"); } } /** * Notifies the user with APNS. */ public void notifyUser(String login, Status status) { log.debug("Notifying user with Apple Push: {}", login); try { String message = "@" + status.getUsername() + "\n" + status.getContent(); if (message.length() > 256) { message = message.substring(0, 252) + "..."; } String payload = APNS.newPayload() .alertBody(message).build(); Collection deviceIds = appleDeviceRepository.findAppleDevices(login); for (String deviceId : deviceIds) { log.debug("Notifying user : {} - device : {}", login, deviceId); apnsService.push(deviceId, payload); } } catch (Exception e) { log.warn("Apple Push error: " + e.getMessage()); } } /** * Check Apple feedback service for inactive devices. */ @Scheduled(cron = "0 0 23 * * ?") public void feedbackService() { log.info("Checking the Apple Feedback Service for inactive devices"); try { Map inactiveDevices = apnsService.getInactiveDevices(); for (String deviceId : inactiveDevices.keySet()) { log.debug("Device {} is inactive", deviceId); String login = appleDeviceUserRepository.findLoginForDeviceId(deviceId); log.debug("Removing device for user {}" + login); appleDeviceRepository.removeAppleDevice(login, deviceId); appleDeviceUserRepository.removeAppleDeviceForUser(deviceId); } } catch (Exception e) { log.warn("Apple Feedback Service error: " + e.getMessage()); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/AtmosphereService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.status.AbstractStatus; import fr.ippon.tatami.service.dto.StatusDTO; import fr.ippon.tatami.web.atmosphere.TatamiNotification; import org.atmosphere.cpr.Broadcaster; import org.atmosphere.cpr.BroadcasterFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.inject.Inject; @Service public class AtmosphereService { private static final Logger log = LoggerFactory.getLogger(AtmosphereService.class); @Inject private TimelineService timelineService; /** * Notifies the user with Atmosphere. */ public void notifyUser(String login, AbstractStatus abstractStatus) { log.debug("Notifying user: {}", login); StatusDTO statusDTO = timelineService.getStatus(abstractStatus.getStatusId()); TatamiNotification notification = new TatamiNotification(); notification.setLogin(login); notification.setStatusDTO(statusDTO); try { Broadcaster broadcaster = BroadcasterFactory .getDefault() .lookup("/realtime/statuses/home_timeline/" + login, true); if (broadcaster != null) { broadcaster.broadcast(notification); } else { log.info("Notification error, the broadcaster is null"); } } catch (Exception e) { log.warn("Notification error: " + e.getMessage()); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/AttachmentService.java ================================================ package fr.ippon.tatami.service; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import javax.imageio.ImageIO; import javax.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; import fr.ippon.tatami.domain.Attachment; import fr.ippon.tatami.domain.DomainConfiguration; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.AttachmentRepository; import fr.ippon.tatami.repository.DomainConfigurationRepository; import fr.ippon.tatami.repository.UserAttachmentRepository; import fr.ippon.tatami.repository.UserRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.exception.StorageSizeException; @Service public class AttachmentService { private static final Logger log = LoggerFactory.getLogger(AttachmentService.class); @Inject private AttachmentRepository attachmentRepository; @Inject private UserAttachmentRepository userAttachmentRepository; @Inject private DomainConfigurationRepository domainConfigurationRepository; @Inject private UserRepository userRepository; @Inject private AuthenticationService authenticationService; @Inject private Environment env; public String createAttachment(Attachment attachment) throws StorageSizeException { User currentUser = authenticationService.getCurrentUser(); DomainConfiguration domainConfiguration = domainConfigurationRepository.findDomainConfigurationByDomain(currentUser.getDomain()); long newAttachmentsSize = currentUser.getAttachmentsSize() + attachment.getSize(); if (newAttachmentsSize > domainConfiguration.getStorageSizeAsLong()) { log.info("User " + currentUser.getLogin() + " has tried to exceed his storage capacity. current storage=" + currentUser.getAttachmentsSize() + ", storage capacity=" + domainConfiguration.getStorageSizeAsLong()); throw new StorageSizeException("User storage exceeded for user " + currentUser.getLogin()); } attachment.setThumbnail(computeThumbnail(attachment)); attachmentRepository.createAttachment(attachment); userAttachmentRepository.addAttachmentId(authenticationService.getCurrentUser().getLogin(), attachment.getAttachmentId()); // Refresh user data, to reduce the risk of errors currentUser = authenticationService.getCurrentUser(); currentUser.setAttachmentsSize(currentUser.getAttachmentsSize() + attachment.getSize()); userRepository.updateUser(currentUser); return attachment.getAttachmentId(); } public Attachment getAttachmentById(String attachmentId) { Attachment attachment = attachmentRepository.findAttachmentById(attachmentId); //Computing the thumbnail if it does not exists if(! attachment.getHasThumbnail()) { attachment.setThumbnail(computeThumbnail(attachment)); attachmentRepository.updateThumbnail(attachment); } return attachment; } public Collection getAttachmentIdsForCurrentUser(int pagination, String finish) { Collection attachmentIds = userAttachmentRepository. findAttachmentIds(authenticationService.getCurrentUser().getLogin(), pagination,finish); log.debug("Collection of attachments : {}", attachmentIds.size()); return attachmentIds; } public void deleteAttachment(Attachment attachment) { log.debug("Removing attachment : {}", attachment); User currentUser = authenticationService.getCurrentUser(); for (String attachmentIdTest : userAttachmentRepository.findAttachmentIds(currentUser.getLogin())) { if (attachmentIdTest.equals(attachment.getAttachmentId())) { userAttachmentRepository.removeAttachmentId(currentUser.getLogin(), attachment.getAttachmentId()); attachmentRepository.deleteAttachment(attachment); // Refresh user data, to reduce the risk of errors currentUser = authenticationService.getCurrentUser(); long newAttachmentsSize = currentUser.getAttachmentsSize() - attachment.getSize(); currentUser.setAttachmentsSize(newAttachmentsSize); userRepository.updateUser(currentUser); break; } } } public Collection getDomainQuota() { User currentUser = authenticationService.getCurrentUser(); DomainConfiguration domainConfiguration = domainConfigurationRepository.findDomainConfigurationByDomain(currentUser.getDomain()); Long domainQuota = domainConfiguration.getStorageSizeAsLong(); Long userQuota = currentUser.getAttachmentsSize(); Long quota = (userQuota * 100) / domainQuota; Collection taux = new ArrayList(); taux.add(quota); log.debug("Domain quota attachments : {}", quota); return taux; } private byte[] computeThumbnail(Attachment attachment) { byte[] result = new byte[0]; String[] imagesExtensions = env.getProperty("tatami.attachment.thumbnail.extensions").split(","); for(String ext : imagesExtensions) { if(attachment.getFilename().endsWith(ext)) { attachment.setHasThumbnail(true); break; } } if(attachment.getHasThumbnail()) { try { BufferedImage thumbnail = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); thumbnail.createGraphics() .drawImage(ImageIO .read(new ByteArrayInputStream(attachment.getContent())) .getScaledInstance(100, 100, BufferedImage.SCALE_SMOOTH), 0, 0, null); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(thumbnail, "png", baos); baos.flush(); result = baos.toByteArray(); } catch(IOException e) { log.error("Error creating thumbnail for attachment "+attachment.getAttachmentId()); } } return result; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/AvatarService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.domain.Avatar; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.AvatarRepository; import fr.ippon.tatami.repository.DomainConfigurationRepository; import fr.ippon.tatami.repository.UserRepository; import fr.ippon.tatami.security.AuthenticationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.imageio.ImageIO; import javax.inject.Inject; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @Service public class AvatarService { private static final Logger log = LoggerFactory.getLogger(AvatarService.class); @Inject private AvatarRepository avatarRepository; @Inject private DomainConfigurationRepository domainConfigurationRepository; @Inject private UserRepository userRepository; @Inject private AuthenticationService authenticationService; public String createAvatar(Avatar avatar) { User currentUser = authenticationService.getCurrentUser(); if (currentUser.getAvatar() != null && !("").equals(currentUser.getAvatar())) { deleteAvatar(currentUser.getAvatar()); } try { avatar.setContent(scaleImage(avatar.getContent())); } catch (IOException e) { log.info("Avatar could not be resized : " + e.getMessage()); return null; } avatarRepository.createAvatar(avatar); log.debug("Avatar created : {}", avatar); return avatar.getAvatarId(); } public Avatar getAvatarById(String avatartId) { log.debug("Get Avatar Id : {}", avatartId); return avatarRepository.findAvatarById(avatartId); } public void deleteAvatar(String avatarId) { avatarRepository.removeAvatar(avatarId); User currentUser = authenticationService.getCurrentUser(); userRepository.updateUser(currentUser); } private byte[] scaleImage(byte[] data) throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(data); BufferedImage img = ImageIO.read(in); int width = Constants.AVATAR_SIZE; int height = Constants.AVATAR_SIZE; Image image = img.getScaledInstance(width, height, Image.SCALE_SMOOTH); BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); bufferedImage.getGraphics().drawImage(image, 0, 0, new Color(0, 0, 0), null); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, "jpg", byteArrayOutputStream); log.debug("New Byte size of Avatar : {} Kbits", byteArrayOutputStream.size() / 1024); return byteArrayOutputStream.toByteArray(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/BlockService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.BlockRepository; import fr.ippon.tatami.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; /** * Created by matthieudelafourniere on 7/7/16. */ @Service public class BlockService { private final Logger log = LoggerFactory.getLogger(BlockService.class); @Inject private BlockRepository blockRepository; @Inject private UserRepository userRepository; public void blockUser(String currentLogin, String blockedLogin){ log.debug(currentLogin + " is blocking " + blockedLogin); blockRepository.blockUser(currentLogin, blockedLogin); } public void unblockUser(String currentLogin, String unblockedLogin){ log.debug(currentLogin + " is unblocking " + unblockedLogin); blockRepository.unblockUser(currentLogin, unblockedLogin); } public Collection getUsersBlockedLoginForUser(String login){ return blockRepository.getUsersBlockedBy(login); } public Collection getUsersBlockedForUser(String login){ Collection blockedUsersLogins = getUsersBlockedLoginForUser(login); Collection blockedUsers = new ArrayList(); for (String blockedLogin : blockedUsersLogins) { User user = userRepository.findUserByLogin(blockedLogin); if(user != null){ blockedUsers.add(user); } } log.debug("Getting users blocked by {}", login); return blockedUsers; } public boolean isBlocked(String blockerLogin, String blockedLogin){ return blockRepository.isBlocked(blockerLogin, blockedLogin); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/CounterService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.repository.CounterRepository; import org.springframework.stereotype.Service; import javax.inject.Inject; /** * Manages the application's counters. * * @author François Descamps */ @Service public class CounterService { @Inject private CounterRepository counterRepository; public long getNbStatus(String login) { return counterRepository.getStatusCounter(login); } public long getNbFollowed(String login) { return counterRepository.getFriendsCounter(login); } public long getNbFollowers(String login) { return counterRepository.getFollowersCounter(login); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/FriendshipService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.MentionFriend; import fr.ippon.tatami.repository.*; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.util.DomainUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * Manages the user's frienships. *

    * - A friend is someone you follow * - A follower is someone that follows you * * @author Julien Dubois */ @Service public class FriendshipService { private final Logger log = LoggerFactory.getLogger(FriendshipService.class); @Inject private UserRepository userRepository; @Inject private FollowerRepository followerRepository; @Inject private FriendRepository friendRepository; @Inject private CounterRepository counterRepository; @Inject private StatusRepository statusRepository; @Inject private MentionlineRepository mentionlineRepository; @Inject private AuthenticationService authenticationService; /** * Follow a user. * * @return true if the operation succeeds, false otherwise */ public boolean followUser(String usernameToFollow) { log.debug("Following user : {}", usernameToFollow); User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); String loginToFollow = DomainUtil.getLoginFromUsernameAndDomain(usernameToFollow, domain); User followedUser = userRepository.findUserByLogin(loginToFollow); if (followedUser != null && !followedUser.equals(currentUser)) { if (counterRepository.getFriendsCounter(currentUser.getLogin()) > 0) { for (String alreadyFollowingTest : friendRepository.findFriendsForUser(currentUser.getLogin())) { if (alreadyFollowingTest.equals(loginToFollow)) { log.debug("User {} already follows user {}", currentUser.getLogin(), followedUser.getLogin()); return false; } } } friendRepository.addFriend(currentUser.getLogin(), followedUser.getLogin()); counterRepository.incrementFriendsCounter(currentUser.getLogin()); followerRepository.addFollower(followedUser.getLogin(), currentUser.getLogin()); counterRepository.incrementFollowersCounter(followedUser.getLogin()); // mention the friend that the user has started following him MentionFriend mentionFriend = statusRepository.createMentionFriend(followedUser.getLogin(), currentUser.getLogin()); mentionlineRepository.addStatusToMentionline(mentionFriend.getLogin(), mentionFriend.getStatusId()); log.debug("User {} now follows user {} ", currentUser.getLogin(), followedUser.getLogin()); return true; } else { log.debug("Followed user does not exist : " + loginToFollow); return false; } } /** * Un-follow a user. * * @return true if the operation succeeds, false otherwise */ public boolean unfollowUser(String usernameToUnfollow) { log.debug("Removing followed user : {}", usernameToUnfollow); User currentUser = authenticationService.getCurrentUser(); String loginToUnfollow = this.getLoginFromUsername(usernameToUnfollow); User userToUnfollow = userRepository.findUserByLogin(loginToUnfollow); return unfollowUser(currentUser, userToUnfollow); } /** * Un-follow a user. * * @return true if the operation succeeds, false otherwise */ public boolean unfollowUser(User currentUser, User userToUnfollow) { if (userToUnfollow != null) { String loginToUnfollow = userToUnfollow.getLogin(); boolean userAlreadyFollowed = false; for (String alreadyFollowingTest : friendRepository.findFriendsForUser(currentUser.getLogin())) { if (alreadyFollowingTest.equals(loginToUnfollow)) { userAlreadyFollowed = true; } } if (userAlreadyFollowed) { friendRepository.removeFriend(currentUser.getLogin(), loginToUnfollow); counterRepository.decrementFriendsCounter(currentUser.getLogin()); followerRepository.removeFollower(loginToUnfollow, currentUser.getLogin()); counterRepository.decrementFollowersCounter(loginToUnfollow); log.debug("User {} has stopped following user {}", currentUser.getLogin(), loginToUnfollow); return true; } else { return false; } } else { log.debug("Followed user does not exist."); return false; } } public List getFriendIdsForUser(String login) { log.debug("Retrieving friends for user : {}", login); return friendRepository.findFriendsForUser(login); } public Collection getFollowerIdsForUser(String login) { log.debug("Retrieving followed users : {}", login); return followerRepository.findFollowersForUser(login); } public Collection getFriendsForUser(String username) { String login = this.getLoginFromUsername(username); Collection friendLogins = friendRepository.findFriendsForUser(login); Collection friends = new ArrayList(); for (String friendLogin : friendLogins) { User friend = userRepository.findUserByLogin(friendLogin); friends.add(friend); } return friends; } public Collection getFollowersForUser(String username) { String login = this.getLoginFromUsername(username); Collection followersLogins = followerRepository.findFollowersForUser(login); Collection followers = new ArrayList(); for (String followerLogin : followersLogins) { User follower = userRepository.findUserByLogin(followerLogin); followers.add(follower); } return followers; } /** * Finds if the "userLogin" user is followed by the current user. */ public boolean isFollowed(String userLogin) { log.debug("Retrieving if you follow this user : {}", userLogin); boolean isFollowed = false; User user = authenticationService.getCurrentUser(); if (null != user && !userLogin.equals(user.getLogin())) { Collection users = getFollowerIdsForUser(userLogin); if (null != users && users.size() > 0) { for (String follower : users) { if (follower.equals(user.getLogin())) { isFollowed = true; break; } } } } return isFollowed; } /** * Finds if the current user user follow the "userLogin". */ public boolean isFollowing(String userLogin) { log.debug("Retrieving if you follow this user : {}", userLogin); boolean isFollowing = false; User user = authenticationService.getCurrentUser(); if (null != user && !userLogin.equals(user.getLogin())) { Collection users = getFriendsForUser(user.getUsername()); if (null != users && users.size() > 0) { for (User follower : users) { if (follower.getUsername().equals(userLogin)) { isFollowing = true; break; } } } } return isFollowing; } private String getLoginFromUsername(String username) { User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); return DomainUtil.getLoginFromUsernameAndDomain(username, domain); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/GroupService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.*; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.dto.UserGroupDTO; import fr.ippon.tatami.service.util.DomainUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.util.Collection; import java.util.Map; import java.util.TreeSet; /** * Service bean for managing groups. */ @Service public class GroupService { private final Logger log = LoggerFactory.getLogger(GroupService.class); @Inject private AuthenticationService authenticationService; @Inject private GroupRepository groupRepository; @Inject private GroupDetailsRepository groupDetailsRepository; @Inject private GroupMembersRepository groupMembersRepository; @Inject private GroupCounterRepository groupCounterRepository; @Inject private UserGroupRepository userGroupRepository; @Inject private UserRepository userRepository; @Inject private SearchService searchService; @Inject private FriendRepository friendRepository; @CacheEvict(value = "group-user-cache", allEntries = true) public void createGroup(String name, String description, boolean publicGroup) { log.debug("Creating group : {}", name); User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); String groupId = groupRepository.createGroup(domain); groupDetailsRepository.createGroupDetails(groupId, name, description, publicGroup); groupMembersRepository.addAdmin(groupId, currentUser.getLogin()); groupCounterRepository.incrementGroupCounter(domain, groupId); userGroupRepository.addGroupAsAdmin(currentUser.getLogin(), groupId); Group group = getGroupById(domain, groupId); searchService.addGroup(group); } @CacheEvict(value = {"group-user-cache", "group-cache"}, allEntries = true) public void editGroup(Group group) { log.debug("Editing group : {}", group.getGroupId()); groupDetailsRepository.editGroupDetails(group.getGroupId(), group.getName(), group.getDescription(), group.isArchivedGroup()); searchService.removeGroup(group); searchService.addGroup(group); } public Collection getMembersForGroup(String groupId, String login) { Map membersMap = groupMembersRepository.findMembers(groupId); Collection friendLogins = friendRepository.findFriendsForUser(login); Collection userGroupDTOs = new TreeSet(); for (Map.Entry member : membersMap.entrySet()) { UserGroupDTO dto = new UserGroupDTO(); User user = userRepository.findUserByLogin(member.getKey()); dto.setLogin(user.getLogin()); dto.setUsername(user.getUsername()); dto.setAvatar(user.getAvatar()); dto.setFirstName(user.getFirstName()); dto.setLastName(user.getLastName()); dto.setRole(member.getValue()); dto.setActivated(user.getActivated()); if (friendLogins.contains(user.getLogin())) { dto.setFriend(true); } if (login.equals(user.getLogin())) { dto.setYou(true); } userGroupDTOs.add(dto); } return userGroupDTOs; } public UserGroupDTO getMembersForGroup(String groupId, User userWanted) { Map membersMap = groupMembersRepository.findMembers(groupId); for (Map.Entry member : membersMap.entrySet()) { User user = userRepository.findUserByLogin(member.getKey()); if (user.getLogin() == userWanted.getLogin()) { UserGroupDTO dto = new UserGroupDTO(); dto.setLogin(user.getLogin()); dto.setUsername(user.getUsername()); dto.setAvatar(user.getAvatar()); dto.setFirstName(user.getFirstName()); dto.setLastName(user.getLastName()); dto.setRole(member.getValue()); return dto; } } return null; } @Cacheable(value = "group-user-cache", key = "#user.login") public Collection getGroupsForUser(User user) { Collection groupIds = userGroupRepository.findGroups(user.getLogin()); return buildGroupIdsList(groupIds); } @Cacheable(value = "group-user-cache", key = "#user.login") public Collection getGroupsOfUser(User user) { Collection groupIds = userGroupRepository.findGroups(user.getLogin()); return getGroupDetails(user, groupIds); } @Cacheable(value = "group-cache") public Group getGroupById(String domain, String groupId) { return internalGetGroupById(domain, groupId); } public Collection getGroupsWhereUserIsAdmin(User user) { Collection groupIds = userGroupRepository.findGroupsAsAdmin(user.getLogin()); return getGroupDetails(user, groupIds); } private Collection getGroupDetails(User currentUser, Collection groupIds) { String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); Collection groups = new TreeSet(); for (String groupId : groupIds) { Group group = internalGetGroupById(domain, groupId); groups.add(group); } return groups; } public Collection getGroupsWhereCurrentUserIsAdmin() { User currentUser = authenticationService.getCurrentUser(); return getGroupsWhereUserIsAdmin(currentUser); } private Group internalGetGroupById(String domain, String groupId) { Group group = groupRepository.getGroupById(domain, groupId); Group groupDetails = groupDetailsRepository.getGroupDetails(groupId); group.setName(groupDetails.getName()); group.setPublicGroup(groupDetails.isPublicGroup()); group.setArchivedGroup(groupDetails.isArchivedGroup()); group.setDescription(groupDetails.getDescription()); long counter = groupCounterRepository.getGroupCounter(domain, groupId); group.setCounter(counter); return group; } @CacheEvict(value = {"group-user-cache", "group-cache"}, allEntries = true) public void addMemberToGroup(User user, Group group) { String groupId = group.getGroupId(); Collection userCurrentGroupIds = userGroupRepository.findGroups(user.getLogin()); boolean userIsAlreadyAMember = false; for (String testGroupId : userCurrentGroupIds) { if (testGroupId.equals(groupId)) { userIsAlreadyAMember = true; } } if (!userIsAlreadyAMember) { groupMembersRepository.addMember(groupId, user.getLogin()); log.debug("user=" + user); groupCounterRepository.incrementGroupCounter(user.getDomain(), groupId); userGroupRepository.addGroupAsMember(user.getLogin(), groupId); } else { log.debug("User {} is already a member of group {}", user.getLogin(), group.getName()); } } @CacheEvict(value = {"group-user-cache", "group-cache"}, allEntries = true) public void removeMemberFromGroup(User user, Group group) { String groupId = group.getGroupId(); Collection userCurrentGroupIds = userGroupRepository.findGroups(user.getLogin()); boolean userIsAlreadyAMember = false; for (String testGroupId : userCurrentGroupIds) { if (testGroupId.equals(groupId)) { userIsAlreadyAMember = true; } } if (userIsAlreadyAMember) { groupMembersRepository.removeMember(groupId, user.getLogin()); groupCounterRepository.decrementGroupCounter(user.getDomain(), groupId); userGroupRepository.removeGroup(user.getLogin(), groupId); } else { log.debug("User {} is not a member of group {}", user.getLogin(), group.getName()); } } public Collection buildGroupList(Collection groups) { User currentUser = authenticationService.getCurrentUser(); return buildGroupList(currentUser, groups); } public Collection buildGroupList(User user, Collection groups) { for (Group group : groups) { buildGroup(user, group); } return groups; } public Group buildGroup(Group group) { User currentUser = authenticationService.getCurrentUser(); return buildGroup(currentUser, group); } private Group getGroupFromUser(User currentUser, String groupId) { Collection groups = getGroupsOfUser(currentUser); for (Group testGroup : groups) { if (testGroup.getGroupId().equals(groupId)) { return testGroup; } } return null; } private boolean isGroupManagedByCurrentUser(Group group) { Collection groups = getGroupsWhereCurrentUserIsAdmin(); boolean isGroupManagedByCurrentUser = false; for (Group testGroup : groups) { if (testGroup.getGroupId().equals(group.getGroupId())) { isGroupManagedByCurrentUser = true; break; } } return isGroupManagedByCurrentUser; } public Group buildGroup(User user, Group group) { if(group != null ) { if (isGroupManagedByCurrentUser(group)) { group.setAdministrator(true); group.setMember(true); } else if(group.isPublicGroup()) { Group result = getGroupFromUser(user, group.getGroupId()); group.setAdministrator(false); // If we made it here, the user is not an admin if (result != null) { group.setMember(true); // We found a group, so the user is a member } else { group.setMember(false); // Since no group was found, the user is not a member } } else { Group result = getGroupFromUser(user, group.getGroupId()); group.setAdministrator(false); // If we make it here, the user is not an admin if (result == null) { log.info("Permission denied! User {} tried to access group ID = {} ", user.getLogin(), group.getGroupId()); group.setMember(false); // No group found, therefore the user is not a member return null; } else { group.setMember(true); // Since a group was found, we know the user is a member } } long counter = 0; for ( UserGroupDTO userGroup : getMembersForGroup(group.getGroupId(),authenticationService.getCurrentUser().getLogin()) ) { if(userGroup.isActivated()) { counter++; } } group.setCounter(counter); } return group; } public Collection buildGroupIdsList(Collection groupIds) { Collection groups = new TreeSet(); for (String groupId : groupIds) { groups.add(buildGroupIds(groupId)); } return groups; } public Group buildGroupIds(String groupId) { User currentUser = authenticationService.getCurrentUser(); return buildGroupIds(currentUser, groupId); } public Group buildGroupIds(User user, String groupId) { String domain = DomainUtil.getDomainFromLogin(user.getLogin()); Group group = getGroupById(domain, groupId); return buildGroup(group); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/MailDigestService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.DigestType; import fr.ippon.tatami.domain.Domain; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.DomainRepository; import fr.ippon.tatami.repository.MailDigestRepository; import fr.ippon.tatami.repository.UserRepository; import fr.ippon.tatami.service.dto.StatusDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; /** * This service generates digest emails for subscribed users. * * @author Pierre Rust */ @Service public class MailDigestService { private static final Logger log = LoggerFactory.getLogger(MailDigestService.class); private final static int MAX_STATUS_DAILY_DIGEST = 10; private final static int MAX_STATUS_WEEKLY_DIGEST = 10; @Inject private MailDigestRepository mailDigestRepository; @Inject private DomainRepository domainRepository; @Inject private UserRepository userRepository; @Inject private TimelineService timelineService; @Inject private MailService mailService; @Inject private SuggestionService suggestionService; /** * Sends daily digest. Must be run every day */ @Scheduled(cron = "0 0 22 * * ?") public void dailyDigest() { log.info("Starting Daily digest mail process "); Set domains = domainRepository.getAllDomains(); String day = String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)); for (Domain d : domains) { log.info("Sending daily digest for domain {} and day {}", d, day); int pagination = 0; List logins; do { logins = mailDigestRepository.getLoginsRegisteredToDigest( DigestType.DAILY_DIGEST, d.getName(), day, pagination); pagination = pagination + logins.size(); for (String login : logins) { try { handleDailyDigestPageForLogin(login); } catch (Exception e) { log.warn("An error has occured when generating daily digest for user " + login + ": " + e.getMessage()); StringWriter stack = new StringWriter(); PrintWriter pw = new PrintWriter(stack); e.printStackTrace(pw); log.debug("{}", stack.toString()); } } } while (logins.size() > 0); } } /** * Sends weekly digest. *

    * Will run every mondayday and send mails to all registered users *

    * If this is too many mails, this method could be tuned to * distribute the load on every day of the week by only sending * mails to users who have subscribed that same day. */ @Scheduled(cron = "0 0 01 ? * MON") public void weeklyDigest() { log.info("Starting Weekly digest mail process "); Set domains = domainRepository.getAllDomains(); // sent digest for all domains // for users that have register any day of the week for (int i = 1; i < 8; ++i) { String day = String.valueOf(i); for (Domain d : domains) { log.info("Sending weekly digest for domain {} and day {}", d, i); int pagination = 0; List logins; do { logins = mailDigestRepository.getLoginsRegisteredToDigest( DigestType.WEEKLY_DIGEST, d.getName(), day, pagination); pagination = pagination + logins.size(); for (String login : logins) { try { handleWeeklyDigestPageForLogin(login); } catch (Exception e) { log.warn("An error has occured when generating weekly digest for user " + login + ": " + e.getMessage()); StringWriter stack = new StringWriter(); PrintWriter pw = new PrintWriter(stack); e.printStackTrace(pw); log.debug("{}", stack.toString()); } } } while (logins.size() > 0); } } } /** * Fetch all necessary info for a daily digest mail * and delegate the sending operation to mailService. */ private void handleDailyDigestPageForLogin(String login) { log.info("Preparing weekly digest for user " + login); User user = userRepository.findUserByLogin(login); // we want statuses for the past 24 hours Calendar cal = Calendar.getInstance(); cal.add(Calendar.DATE, -1); Date yesterday = cal.getTime(); List digestStatuses = new ArrayList(MAX_STATUS_DAILY_DIGEST); int nbStatusTotal = getStatusesForDigest(user, yesterday, MAX_STATUS_DAILY_DIGEST, digestStatuses); Collection suggestedUsers = suggestionService.suggestUsers(user.getLogin()); // TODO : we could look for popular messages // especially if the user // does not have anything in it's timeline and there are no suggested users for him mailService.sendDailyDigestEmail(user, digestStatuses, nbStatusTotal, suggestedUsers); } /** * Fetch all necessary info for a weekly digest mail * and delegate the sending operation to mailService. */ private void handleWeeklyDigestPageForLogin(String login) { log.info("Preparing weekly digest for user " + login); User user = userRepository.findUserByLogin(login); // we want statuses for the past week Calendar cal = Calendar.getInstance(); cal.add(Calendar.DATE, -7); Date lastWeek = cal.getTime(); List digestStatuses = new ArrayList(MAX_STATUS_WEEKLY_DIGEST); int nbStatusTotal = getStatusesForDigest(user, lastWeek, MAX_STATUS_WEEKLY_DIGEST, digestStatuses); Collection suggestedUsers = suggestionService.suggestUsers(user.getLogin()); Collection suggestedGroups = suggestionService.suggestGroups(user.getLogin()); mailService.sendWeeklyDigestEmail(user, digestStatuses, nbStatusTotal, suggestedUsers, suggestedGroups); } /** * Build a list containing an extract of the status from an user timeline, * except its own, since a given date. * * @param user the user * @param since_date date since * @param nbStatus number of status to include in the extract * @param digestStatuses selected status will be added to this list (ordered by date) * @return the number of statuses */ private int getStatusesForDigest(final User user, final Date since_date, int nbStatus, List digestStatuses) { String finish = null; boolean dateReached = false; List allStatuses = new ArrayList(50); // collect all statuses since 'since_date' from the timeline while (!dateReached) { Collection statuses = timelineService.getUserTimeline(user.getLogin(), 200, null, finish); statuses.size(); int count = 0; if (statuses.isEmpty()) { dateReached = true; } for (StatusDTO status : statuses) { if (status.getStatusDate().before(since_date)) { dateReached = true; break; } else { // Do not includes user's own status in digest if (!status.getUsername().equals(user.getUsername())) { allStatuses.add(status); } } count++; if (count == statuses.size() && !dateReached) { finish = status.getStatusId(); } } } int nbStatusTotal = allStatuses.size(); if (nbStatusTotal > 0) { // now select some of theses statuses if (allStatuses.size() > nbStatus) { Collections.shuffle(allStatuses); digestStatuses.addAll(allStatuses.subList(0, nbStatus)); Collections.sort(digestStatuses, new Comparator() { @Override public int compare(StatusDTO statusDTO, StatusDTO statusDTO2) { return statusDTO.getStatusDate().compareTo(statusDTO2.getStatusDate()); } }); } else { digestStatuses.addAll(allStatuses); } } return nbStatusTotal; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/MailService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.AbstractStatus; import fr.ippon.tatami.domain.status.Status; import fr.ippon.tatami.repository.DomainRepository; import fr.ippon.tatami.repository.UserRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.security.TatamiUserDetailsService; import fr.ippon.tatami.service.dto.StatusDTO; import fr.ippon.tatami.service.util.DomainUtil; import org.apache.velocity.app.VelocityEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import org.springframework.core.env.Environment; import org.springframework.mail.MailException; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.ui.velocity.VelocityEngineUtils; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.mail.MessagingException; import java.util.*; /** * Send e-mails. *

    * Templates are implemented with velocity. * Emails localisation is based on system locale, this could be improved by storing a preferred locale * for each user. * * @author Julien Dubois */ @Service public class MailService { private static final Logger log = LoggerFactory.getLogger(MailService.class); @Inject private Environment env; @Inject private MessageSource mailMessageSource; @Inject private AuthenticationService authenticationService; @Inject private UserService userService; @Inject private DomainRepository domainRepository; private String host; private int port; private String smtpUser; private String smtpPassword; private String smtpTls; private String from; private String tatamiUrl; private Locale locale; // TODO: this can be used for external mail template configuration private final String templateRoot = "/META-INF/tatami/mails/"; private final String templateSuffix = "Email"; @Inject private VelocityEngine velocityEngine; @Inject private TatamiUserDetailsService tatamiUserDetailsService; @PostConstruct public void init() { this.host = env.getProperty("smtp.host"); if (!env.getProperty("smtp.port").equals("")) { this.port = env.getProperty("smtp.port", Integer.class); } this.smtpUser = env.getProperty("smtp.user"); this.smtpPassword = env.getProperty("smtp.password"); this.smtpTls = env.getProperty("smtp.tls"); this.from = env.getProperty("smtp.from"); this.tatamiUrl = env.getProperty("tatami.url"); // TODO : we should probably let the user choose which language he wants to use for email, and store this as a preference this.locale = Locale.getDefault(); } @Async public void sendRegistrationEmail(String registrationKey, User user) { String registrationUrl = tatamiUrl + "/#/register?key=" + registrationKey; log.debug("Sending registration e-mail to User '{}', Url='{}' " + "with locale : '{}'", user.getLogin(), registrationUrl, locale); Map model = new HashMap(); model.put("user", user); model.put("registrationUrl", registrationUrl); sendTextFromTemplate(user.getLogin(), model, "registration", this.locale); } @Async public void sendInvitationEmail(String email, User user) { log.debug("Sending invitation e-mail to email '{}'", email); Map model = new HashMap(); model.put("user", user.getLogin()); model.put("invitationUrl", tatamiUrl); sendTextFromTemplate(user.getLogin(), model, "invitationMessage", this.locale); } @Async public void sendLostPasswordEmail(String registrationKey, User user) { String url = tatamiUrl + "/#/register?key=" + registrationKey; log.debug("Sending lost password e-mail to User '{}', Url='{}' with locale : '{}'", user.getLogin(), url, locale); Map model = new HashMap(); model.put("user", user); model.put("reinitUrl", url); sendTextFromTemplate(user.getLogin(), model, "lostPassword", this.locale); } @Async public void sendValidationEmail(User user, String password) { log.debug("Sending validation e-mail to User '{}', non-encrypted Password='{}' " + "with locale : '{}'", user.getLogin(), password, locale); Map model = new HashMap(); model.put("user", user); model.put("password", password); sendTextFromTemplate(user.getLogin(), model, "validation", this.locale); } @Async public void sendPasswordReinitializedEmail(User user, String password) { log.debug("Sending password re-initialization e-mail to User '{}', non-encrypted Password='{}' " + "with locale : '{}'", user.getLogin(), password, locale); Map model = new HashMap(); model.put("user", user); model.put("password", password); sendTextFromTemplate(user.getLogin(), model, "passwordReinitialized", this.locale); } @Async public void sendUserPrivateMessageEmail(User mentionnedUser, Status status) { log.debug("Sending Private Message e-mail to User '{}' " + "with locale : '{}'", mentionnedUser.getLogin(), locale); String url = tatamiUrl + "/#/home/status/" + status.getStatusId(); Map model = new HashMap(); model.put("user", mentionnedUser); model.put("status", status); model.put("statusUrl", url); sendTextFromTemplate(mentionnedUser.getLogin(), model, "userPrivateMessage", this.locale); } @Async public void sendUserMentionEmail(User mentionnedUser, Status status) { log.debug("Sending Mention e-mail to User '{}' with locale : '{}'", mentionnedUser.getLogin(), locale); String url = tatamiUrl + "/#/home/status/" + status.getStatusId(); Map model = new HashMap(); model.put("user", mentionnedUser); model.put("status", status); model.put("statusUrl", url); sendTextFromTemplate(mentionnedUser.getLogin(), model, "userMention", this.locale); } @Async public void sendDailyDigestEmail(User user, List statuses, int nbStatus, Collection suggestedUsers) { log.debug("Sending daily digest e-mail to User '{}'", user.getLogin()); Map model = new HashMap(); model.put("user", user); model.put("tatamiUrl", tatamiUrl); model.put("statuses", statuses); model.put("nbStatus", nbStatus); model.put("suggestedUsers", suggestedUsers); sendTextFromTemplate(user.getLogin(), model, "dailyDigest", this.locale); } @Async public void sendWeeklyDigestEmail(User user, List statuses, int nbStatus, Collection suggestedUsers, Collection suggestedGroup) { log.debug("Sending weekly digest e-mail to User '{}'", user.getLogin()); Map model = new HashMap(); model.put("user", user); model.put("tatamiUrl", tatamiUrl); model.put("statuses", statuses); model.put("nbStatus", nbStatus); model.put("suggestedUsers", suggestedUsers); model.put("suggestedGroups", suggestedGroup); sendTextFromTemplate(user.getLogin(), model, "weeklyDigest", this.locale); } @Async public void sendReportedStatusEmail(String emailReporting, AbstractStatus status) { log.debug("Sending reported status e-mail to User '{}' with locale : '{}'", locale); String url = tatamiUrl + "/#/home/status/" + status.getStatusId(); Map model = new HashMap(); model.put("status", status); model.put("statusUrl", url); for(String email : tatamiUserDetailsService.getAdminUsers()) { sendTextFromTemplate(email, model, "reportedStatus", this.locale); } } @Async public void sendDeactivatedEmail(String email) { log.debug("Sending deactivation e-mail to User '{}' with locale : '{}'", email, locale); Map model = new HashMap(); model.put("deactivatedEmail", email); model.put("username", DomainUtil.getUsernameFromLogin(email)); sendTextFromTemplate(email, model, "deactivatedUser", this.locale); } public boolean connectSmtpServer() { if (host != null && !host.equals("")) { JavaMailSenderImpl sender = configureJavaMailSender(); try { sender.getSession().getTransport().connect(); return true; } catch (MessagingException e) { return false; } } else { return false; } } private void sendEmail(String email, String subject, String text) { if (host != null && !host.equals("")) { JavaMailSenderImpl sender = configureJavaMailSender(); SimpleMailMessage message = new SimpleMailMessage(); message.setTo(email); message.setFrom(from); message.setSubject(subject); message.setText(text); try { sender.send(message); log.debug("Sent e-mail to User '{}'!", email); } catch (MailException e) { log.warn("Warning! SMTP server error, could not send e-mail."); log.debug("SMTP Error : {}", e.getMessage()); log.debug("Did you configure your SMTP settings in /META-INF/tatami/tatami.properties ?"); } } else { log.debug("SMTP server is not configured in /META-INF/tatami/tatami.properties"); } } private JavaMailSenderImpl configureJavaMailSender() { JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setHost(host); sender.setPort(port); sender.setUsername(smtpUser); sender.setPassword(smtpPassword); if (smtpTls != null && !smtpTls.equals("")) { Properties sendProperties = new Properties(); sendProperties.setProperty("mail.smtp.starttls.enable", "true"); sender.setJavaMailProperties(sendProperties); } return sender; } /** * Generate and send the mail corresponding to the given template. */ private void sendTextFromTemplate(String email, Map model, String template, Locale locale) { model.put("messages", mailMessageSource); model.put("locale", locale); String subject = mailMessageSource.getMessage(template + ".title", null, locale); String text = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, templateRoot + template + templateSuffix, "utf-8", model); log.debug("e-mail text '{}", text); sendEmail(email, subject, text); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/MentionService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.Status; import fr.ippon.tatami.repository.MentionlineRepository; import fr.ippon.tatami.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.inject.Inject; /** * Notifies a user when he is mentionned. */ @Service public class MentionService { @Inject private MentionlineRepository mentionlineRepository; @Inject private MailService mailService; @Autowired(required = false) private ApplePushService applePushService; @Inject private UserRepository userRepository; /** * A status that mentions a user is put in the user's mentionline and in his timeline. * The mentioned user can also be notified by email. */ public void mentionUser(String mentionedLogin, Status status) { mentionlineRepository.addStatusToMentionline(mentionedLogin, status.getStatusId()); User mentionnedUser = userRepository.findUserByLogin(mentionedLogin); if (mentionnedUser != null && (mentionnedUser.getPreferencesMentionEmail() == null || mentionnedUser.getPreferencesMentionEmail().equals(true))) { if (status.getStatusPrivate()) { // Private status mailService.sendUserPrivateMessageEmail(mentionnedUser, status); if (applePushService != null) { applePushService.notifyUser(mentionedLogin, status); } } else { mailService.sendUserMentionEmail(mentionnedUser, status); if (applePushService != null) { applePushService.notifyUser(mentionedLogin, status); } } } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/SearchService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.Status; import java.util.Collection; import java.util.List; /** * Service used to search statuses and users. */ public interface SearchService { public static final int DEFAULT_PAGE_SIZE = 20; public static final int DEFAULT_TOP_N_SEARCH_USER = 8; /** * Reset the search engine. *

    * This is used to do a full reindexation of all the data. * * @return if the reset was completed OK */ boolean reset(); /** * Add a status to the index. * * @param status the status to add : can't be null */ void addStatus(Status status); void addStatuses(Collection statuses); /** * Delete a status from the index. * * @param status the status to delete */ void removeStatus(Status status); /** * Search an item in the index. * * @param query the query : mandatory * @param page the page to return * @param size the size of a page */ List searchStatus(String domain, String query, int page, int size); /** * Add a user to the index. * * @param user the user to add : can't be null */ void addUser(User user); void addUsers(Collection users); void removeUser(User user); Collection searchUserByPrefix(String domain, String prefix); void addGroup(Group group); void removeGroup(Group group); Collection searchGroupByPrefix(String domain, String prefix, int size); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/StatsService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.UserStatusStat; import fr.ippon.tatami.repository.DaylineRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.util.DomainUtil; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; @Service public class StatsService { @Inject private AuthenticationService authenticationService; @Inject private DaylineRepository daylineRepository; static final SimpleDateFormat DAYLINE_KEY_FORMAT = new SimpleDateFormat("ddMMyyyy"); /** * The dayline contains a day's status. * * @return a status list */ public Collection getDayline() { Date today = new Date(); return getDayline(today); } /** * The dayline contains a day's status. * * @param date the day to retrieve the status of * @return a status list */ public Collection getDayline(Date date) { if (date == null) { date = new Date(); } User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); String day = DAYLINE_KEY_FORMAT.format(date); return daylineRepository.getDayline(domain, day); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/StatusUpdateService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.*; import fr.ippon.tatami.repository.*; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.dto.StatusDTO; import fr.ippon.tatami.service.exception.ArchivedGroupException; import fr.ippon.tatami.service.exception.ReplyStatusException; import fr.ippon.tatami.service.util.DomainUtil; import org.apache.camel.util.Time; import org.apache.commons.lang.StringEscapeUtils; import org.apache.openjpa.jdbc.kernel.exps.Abs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @Service public class StatusUpdateService { private static final Logger log = LoggerFactory.getLogger(StatusUpdateService.class); private static final Pattern PATTERN_LOGIN = Pattern.compile("@[^\\s,\\p{Punct}]+"); private static final Pattern PATTERN_HASHTAG = Pattern.compile("(^|\\s)#([^\\s !\"#$%&\'()*+,./:;<=>?@\\\\\\[\\]^_`{|}~-]+)"); @Inject private FollowerRepository followerRepository; @Inject private TagFollowerRepository tagFollowerRepository; @Inject private AuthenticationService authenticationService; @Inject private DaylineRepository daylineRepository; @Inject private StatusRepository statusRepository; @Inject private TimelineRepository timelineRepository; @Inject private MentionService mentionService; @Inject private UserlineRepository userlineRepository; @Inject private TaglineRepository taglineRepository; @Inject private TagCounterRepository tagCounterRepository; @Inject private UserGroupRepository userGroupRepository; @Inject private GrouplineRepository grouplineRepository; @Inject private GroupMembersRepository groupMembersRepository; @Inject private GroupService groupService; @Inject private TrendRepository trendsRepository; @Inject private UserTrendRepository userTrendRepository; @Inject private DiscussionRepository discussionRepository; @Inject private CounterRepository counterRepository; @Inject private SearchService searchService; @Inject private DomainlineRepository domainlineRepository; @Inject private StatusAttachmentRepository statusAttachmentRepository; @Inject private AtmosphereService atmosphereService; @Inject private MailService mailService; @Inject private StatusReportRepository statusReportRepository; @Inject private BlockRepository blockRepository; @Inject private TimelineService timelineService; public void postStatus(String content, boolean statusPrivate, Collection attachmentIds, String geoLocalization) { createStatus(content, statusPrivate, null, "", "", "", attachmentIds, null, geoLocalization); } public void postStatus(String content, boolean statusPrivate, Collection attachmentIds) { createStatus(content, statusPrivate, null, "", "", "", attachmentIds); } public void postStatusToGroup(String content, Group group, Collection attachmentIds, String geoLocalization) { createStatus(content, false, group, "", "", "", attachmentIds, null, geoLocalization); } public void postStatusAsUser(String content, User user) { createStatus(content, false, null, "", "", "", null, user, null); } public void replyToStatus(String content, String replyTo, Collection attachmentIds) throws ArchivedGroupException, ReplyStatusException { AbstractStatus abstractStatus = statusRepository.findStatusById(replyTo); if (abstractStatus != null && !abstractStatus.getType().equals(StatusType.STATUS) && !abstractStatus.getType().equals(StatusType.SHARE) && !abstractStatus.getType().equals(StatusType.ANNOUNCEMENT)) { log.debug("Cannot reply to a status of this type"); throw new ReplyStatusException(); } if (abstractStatus != null && abstractStatus.getType().equals(StatusType.SHARE)) { log.debug("Replacing the share by the original status"); Share share = (Share) abstractStatus; AbstractStatus abstractRealStatus = statusRepository.findStatusById(share.getOriginalStatusId()); abstractStatus = abstractRealStatus; } else if (abstractStatus != null && abstractStatus.getType().equals(StatusType.ANNOUNCEMENT)) { log.debug("Replacing the announcement by the original status"); Announcement announcement = (Announcement) abstractStatus; AbstractStatus abstractRealStatus = statusRepository.findStatusById(announcement.getOriginalStatusId()); abstractStatus = abstractRealStatus; } Status status = (Status) abstractStatus; Group group = null; if (status.getGroupId() != null) { group = groupService.getGroupById(status.getDomain(), status.getGroupId()); if (group.isArchivedGroup()) { throw new ArchivedGroupException(); } } if (!status.getReplyTo().equals("")) { log.debug("Replacing the status by the status at the origin of the discussion"); // Original status is also a reply, replying to the real original status instead AbstractStatus abstractRealOriginalStatus = statusRepository.findStatusById(status.getDiscussionId()); if (abstractRealOriginalStatus == null || !abstractRealOriginalStatus.getType().equals(StatusType.STATUS)) { throw new ReplyStatusException(); } Status realOriginalStatus = (Status) abstractRealOriginalStatus; Status replyStatus = createStatus( content, realOriginalStatus.getStatusPrivate(), group, realOriginalStatus.getStatusId(), status.getStatusId(), status.getUsername(), attachmentIds); discussionRepository.addReplyToDiscussion(realOriginalStatus.getStatusId(), replyStatus.getStatusId()); } else { log.debug("Replying directly to the status at the origin of the disucssion"); // The original status of the discussion is the one we reply to Status replyStatus = createStatus(content, status.getStatusPrivate(), group, status.getStatusId(), status.getStatusId(), status.getUsername(), attachmentIds); discussionRepository.addReplyToDiscussion(status.getStatusId(), replyStatus.getStatusId()); } } private Status createStatus(String content, boolean statusPrivate, Group group, String discussionId, String replyTo, String replyToUsername, Collection attachmentIds) { return createStatus( content, statusPrivate, group, discussionId, replyTo, replyToUsername, attachmentIds, null, null); } private Status createStatus(String content, boolean statusPrivate, Group group, String discussionId, String replyTo, String replyToUsername, Collection attachmentIds, User user, String geoLocalization) { content = StringEscapeUtils.unescapeHtml(content); long startTime = 0; if (log.isInfoEnabled()) { startTime = Calendar.getInstance().getTimeInMillis(); log.debug("Creating new status : {}", content); } String currentLogin; if (user == null) { currentLogin = authenticationService.getCurrentUser().getLogin(); } else { currentLogin = user.getLogin(); } String domain = DomainUtil.getDomainFromLogin(currentLogin); Status status = statusRepository.createStatus(currentLogin, statusPrivate, group, attachmentIds, content, discussionId, replyTo, replyToUsername, geoLocalization); if (attachmentIds != null && attachmentIds.size() > 0) { for (String attachmentId : attachmentIds) { statusAttachmentRepository.addAttachmentId(status.getStatusId(), attachmentId); } } // add status to the timeline addStatusToTimelineAndNotify(currentLogin, status); if (status.getStatusPrivate()) { // Private status // add status to the mentioned users' timeline manageMentions(status, null, currentLogin, domain, new ArrayList()); } else { // Public status Collection followersForUser = followerRepository.findFollowersForUser(currentLogin); // add status to the dayline, userline String day = StatsService.DAYLINE_KEY_FORMAT.format(status.getStatusDate()); daylineRepository.addStatusToDayline(status, day); userlineRepository.addStatusToUserline(status.getLogin(), status.getStatusId()); // add the status to the group line and group followers manageGroups(status, group, followersForUser); // tag managgement manageStatusTags(status, group); // add status to the mentioned users' timeline manageMentions(status, group, currentLogin, domain, followersForUser); // Increment status count for the current user counterRepository.incrementStatusCounter(currentLogin); // Add to the searchStatus engine searchService.addStatus(status); // add status to the company wall addToCompanyWall(status, group); } if (log.isInfoEnabled()) { long finishTime = Calendar.getInstance().getTimeInMillis(); log.info("Status created in " + (finishTime - startTime) + "ms."); } return status; } private void manageGroups(Status status, Group group, Collection followersForUser) { if (group != null) { grouplineRepository.addStatusToGroupline(group.getGroupId(), status.getStatusId()); Collection groupMemberLogins = groupMembersRepository.findMembers(group.getGroupId()).keySet(); // For all people following the group for (String groupMemberLogin : groupMemberLogins) { addStatusToTimelineAndNotify(groupMemberLogin, status); } if (isPublicGroup(group)) { // for people not following the group but following the user for (String followerLogin : followersForUser) { if (!groupMemberLogins.contains(followerLogin)) { addStatusToTimelineAndNotify(followerLogin, status); } } } } else { // only people following the user for (String followerLogin : followersForUser) { addStatusToTimelineAndNotify(followerLogin, status); } } } private void addToCompanyWall(Status status, Group group) { if (isPublicGroup(group)) { domainlineRepository.addStatusToDomainline(status.getDomain(), status.getStatusId()); } } /** * Parses the status to find tags, and add those tags to the TagLine and the Trends. *

    * The Tatami Bot is a specific use case : as it sends a lot of statuses, it may pollute the global trends, * so it is excluded from it. */ private void manageStatusTags(Status status, Group group) { Matcher m = PATTERN_HASHTAG.matcher(status.getContent()); while (m.find()) { String tag = m.group(2); if (tag != null && !tag.isEmpty() && !tag.contains("#")) { log.debug("Found tag : {}", tag); taglineRepository.addStatusToTagline(tag, status); tagCounterRepository.incrementTagCounter(status.getDomain(), tag); //Excludes the Tatami Bot from the global trend if (!status.getUsername().equals(Constants.TATAMIBOT_NAME)) { trendsRepository.addTag(status.getDomain(), tag); } userTrendRepository.addTag(status.getLogin(), tag); // Add the status to all users following this tag addStatusToTagFollowers(status, group, tag); } } } private void manageMentions(Status status, Group group, String currentLogin, String domain, Collection followersForUser) { Matcher m = PATTERN_LOGIN.matcher(status.getContent()); while (m.find()) { String mentionedUsername = extractUsernameWithoutAt(m.group()); if (mentionedUsername != null && !mentionedUsername.equals(currentLogin) && !followersForUser.contains(mentionedUsername)) { log.debug("Mentionning : {}", mentionedUsername); String mentionedLogin = DomainUtil.getLoginFromUsernameAndDomain(mentionedUsername, domain); // If this is a private group, and if the mentioned user is not in the group, he will not see the status if (!isPublicGroup(group)) { Collection groupIds = userGroupRepository.findGroups(mentionedLogin); if (groupIds.contains(group.getGroupId())) { // The user is part of the private group mentionUser(mentionedLogin, status); } } else { // This is a public status mentionUser(mentionedLogin, status); } } } } private void addStatusToTagFollowers(Status status, Group group, String tag) { Collection followersForTag = tagFollowerRepository.findFollowers(status.getDomain(), tag); if (isPublicGroup(group)) { // This is a public status for (String followerLogin : followersForTag) { addStatusToTimelineAndNotify(followerLogin, status); } } else { // This is a private status for (String followerLogin : followersForTag) { Collection groupIds = userGroupRepository.findGroups(followerLogin); if (groupIds.contains(group.getGroupId())) { // The user is part of the private group addStatusToTimelineAndNotify(followerLogin, status); } } } } /** * A status that mentions a user is put in the user's mentionline and in his timeline. * The mentioned user can also be notified by email or iOS push. */ private void mentionUser(String mentionedLogin, Status status) { addStatusToTimelineAndNotify(mentionedLogin, status); mentionService.mentionUser(mentionedLogin, status); } private String extractUsernameWithoutAt(String dest) { return dest.substring(1, dest.length()); } private boolean isPublicGroup(Group group) { return group == null || group.isPublicGroup(); } /** * Adds the status to the timeline and notifies the user with Atmosphere. */ private void addStatusToTimelineAndNotify(String login, Status status) { timelineRepository.addStatusToTimeline(login, status.getStatusId()); atmosphereService.notifyUser(login, status); } public void reportStatus(String reportingLogin, String statusId) { log.debug("Reported Status: ", statusId); String domain = DomainUtil.getDomainFromLogin(reportingLogin); statusReportRepository.reportStatus(domain, statusId, reportingLogin); mailService.sendReportedStatusEmail(reportingLogin, statusRepository.findStatusById(statusId)); } private List getAllReportedStatuses(String domain){ return statusReportRepository.findReportedStatuses(domain); } public Collection findReportedStatuses (){ List reportedStatusId = getAllReportedStatuses(authenticationService.getCurrentUser().getDomain()); return timelineService.buildStatusList(reportedStatusId); } public void approveReportedStatus(String statusId){ if(authenticationService.hasAuthenticatedUser() && authenticationService.isCurrentUserInRole("ROLE_ADMIN")){ log.debug("Admin approving reported status {}", statusId ); User currentUser = authenticationService.getCurrentUser(); statusReportRepository.unreportStatus(currentUser.getDomain(), statusId); } else { log.warn("Attempt to approve reported status {} but is not admin", statusId); } } public void deleteReportedStatus(String statusId){ if(authenticationService.hasAuthenticatedUser() && authenticationService.isCurrentUserInRole("ROLE_ADMIN")){ log.debug("Admin deleting reported status {}", statusId ); User currentUser = authenticationService.getCurrentUser(); statusReportRepository.unreportStatus(currentUser.getDomain(), statusId); timelineService.removeStatus(statusId); } else { log.warn("Attempt to delete reported status {} but is not admin", statusId); } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/SuggestionService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.UserGroupRepository; import fr.ippon.tatami.service.util.DomainUtil; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.util.*; import static fr.ippon.tatami.service.util.AnalysisUtil.findMostUsedKeys; import static fr.ippon.tatami.service.util.AnalysisUtil.incrementKeyCounterInMap; import static fr.ippon.tatami.service.util.AnalysisUtil.reduceCollectionSize; /** * Analyses data to find suggestions of users, groups... for the current user. */ @Service public class SuggestionService { private static final int SAMPLE_SIZE = 20; private static final int SUB_SAMPLE_SIZE = 100; private static final int SUGGESTIONS_SIZE = 3; @Inject private FriendshipService friendshipService; @Inject private UserGroupRepository userGroupRepository; @Inject private UserService userService; @Inject private GroupService groupService; /** * Size of the sample data used to find the suggestions. */ @Cacheable("suggest-users-cache") public Collection suggestUsers(String login) { Map userCount = new HashMap(); List friendIds = friendshipService.getFriendIdsForUser(login); List sampleFriendIds = reduceCollectionSize(friendIds, SAMPLE_SIZE); for (String friendId : sampleFriendIds) { List friendsOfFriend = friendshipService.getFriendIdsForUser(friendId); friendsOfFriend = reduceCollectionSize(friendsOfFriend, SUB_SAMPLE_SIZE); for (String friendOfFriend : friendsOfFriend) { if (!friendIds.contains(friendOfFriend) && !friendOfFriend.equals(login)) { incrementKeyCounterInMap(userCount, friendOfFriend); } } } List mostFollowedUsers = findMostUsedKeys(userCount); List userSuggestions = new ArrayList(); for (String mostFollowedUser : mostFollowedUsers) { User suggestion = userService.getUserByLogin(mostFollowedUser); if ( suggestion.getActivated() ){ userSuggestions.add(suggestion); } } if (userSuggestions.size() > SUGGESTIONS_SIZE) { return userSuggestions.subList(0, SUGGESTIONS_SIZE); } else { return userSuggestions; } } @Cacheable("suggest-groups-cache") public Collection suggestGroups(String login) { Map groupCount = new HashMap(); List groupIds = userGroupRepository.findGroups(login); List friendIds = friendshipService.getFriendIdsForUser(login); friendIds = reduceCollectionSize(friendIds, SAMPLE_SIZE); for (String friendId : friendIds) { List groupsOfFriend = userGroupRepository.findGroups(friendId); for (String groupOfFriend : groupsOfFriend) { if (!groupIds.contains(groupOfFriend)) { incrementKeyCounterInMap(groupCount, groupOfFriend); } } } List mostFollowedGroups = findMostUsedKeys(groupCount); List groupSuggestions = new ArrayList(); String domain = DomainUtil.getDomainFromLogin(login); for (String mostFollowedGroup : mostFollowedGroups) { Group suggestion = groupService.getGroupById(domain, mostFollowedGroup); if (suggestion.isPublicGroup()) { // Only suggest public groups for the moment groupSuggestions.add(suggestion); } } if (groupSuggestions.size() > SUGGESTIONS_SIZE) { return groupSuggestions.subList(0, SUGGESTIONS_SIZE); } else { return groupSuggestions; } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/TagMembershipService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.TagFollowerRepository; import fr.ippon.tatami.repository.UserTagRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.util.DomainUtil; import fr.ippon.tatami.web.rest.dto.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.inject.Inject; /** * Manages the tag memberships. *

    * - A tag follower is someone who follows a tag * - A user tag is a tag followed by a user * * @author Julien Dubois */ @Service public class TagMembershipService { private final Logger log = LoggerFactory.getLogger(TagMembershipService.class); @Inject private TagFollowerRepository tagFollowerRepository; @Inject private UserTagRepository userTagRepository; @Inject private AuthenticationService authenticationService; public boolean followTag(Tag tag) { log.debug("Following tag : {}", tag); User currentUser = authenticationService.getCurrentUser(); for (String alreadyFollowingTest : userTagRepository.findTags(currentUser.getLogin())) { if (alreadyFollowingTest.equals(tag.getName())) { log.debug("User {} already follows tag {}", currentUser.getLogin(), tag); return false; } } String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); userTagRepository.addTag(currentUser.getLogin(), tag.getName()); tagFollowerRepository.addFollower(domain, tag.getName(), currentUser.getLogin()); log.debug("User " + currentUser.getLogin() + " now follows tag " + tag); return true; } public boolean unfollowTag(Tag tag) { log.debug("Removing followed tag : {}", tag); User currentUser = authenticationService.getCurrentUser(); boolean tagAlreadyFollowed = false; for (String alreadyFollowingTest : userTagRepository.findTags(currentUser.getLogin())) { if (alreadyFollowingTest.equals(tag.getName())) { tagAlreadyFollowed = true; } } if (tagAlreadyFollowed) { String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); userTagRepository.removeTag(currentUser.getLogin(), tag.getName()); tagFollowerRepository.removeFollower(domain, tag.getName(), currentUser.getLogin()); log.debug("User " + currentUser.getLogin() + " has stopped following tag " + tag); return true; } else { return false; } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/TimelineService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.*; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.security.DomainViolationException; import fr.ippon.tatami.service.dto.StatusDTO; import fr.ippon.tatami.service.util.DomainUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import fr.ippon.tatami.domain.status.*; import javax.inject.Inject; import java.util.*; /** * Manages the timeline. * * @author Julien Dubois */ @Service public class TimelineService { private static final Logger log = LoggerFactory.getLogger(TimelineService.class); private static final String hashtagDefault = "---"; @Inject private UserService userService; @Inject private StatusRepository statusRepository; @Inject private SharesRepository sharesRepository; @Inject private DiscussionRepository discussionRepository; @Inject private CounterRepository counterRepository; @Inject private TimelineRepository timelineRepository; @Inject private MentionlineRepository mentionlineRepository; @Inject private UserlineRepository userlineRepository; @Inject private FavoritelineRepository favoritelineRepository; @Inject private TaglineRepository taglineRepository; @Inject private GrouplineRepository grouplineRepository; @Inject private DomainlineRepository domainlineRepository; @Inject private DomainRepository domainRepository; @Inject private FollowerRepository followerRepository; @Inject private AuthenticationService authenticationService; @Inject private GroupService groupService; @Inject private SearchService searchService; @Inject private AtmosphereService atmosphereService; @Inject private BlockService blockService; public StatusDTO getStatus(String statusId) { List line = new ArrayList(); line.add(statusId); Collection statusCollection = buildStatusList(line); if (statusCollection.isEmpty()) { return null; } else { StatusDTO statusDTO = statusCollection.iterator().next(); // Private message check if (statusDTO.isStatusPrivate()) { String login = authenticationService.getCurrentUser().getLogin(); if (!timelineRepository.isStatusInTimeline(login, statusId)) { log.info("User " + login + " tried to access private message ID " + statusId); return null; } } return statusDTO; } } /** * Get the details for a status * - Who shared this status * - The discussion in which this status belongs to */ public StatusDetails getStatusDetails(String statusId) { log.debug("Looking for status details"); StatusDetails details = new StatusDetails(); AbstractStatus abstractStatus = statusRepository.findStatusById(statusId); if (abstractStatus == null) { log.debug("Status could not be found"); return null; } Status status = null; if (abstractStatus.getType() == null || abstractStatus.getType().equals(StatusType.STATUS)) { status = (Status) abstractStatus; } else if (abstractStatus.getType().equals(StatusType.SHARE)) { Share share = (Share) abstractStatus; AbstractStatus originalStatus = statusRepository.findStatusById(share.getOriginalStatusId()); if (originalStatus == null) { log.debug("Original Status could not be found"); return details; } else if (originalStatus.getType() != null && !originalStatus.getType().equals(StatusType.STATUS)) { log.debug("Original status does not have the correct type"); return details; } status = (Status) originalStatus; } else if (abstractStatus.getType().equals(StatusType.ANNOUNCEMENT)) { Announcement announcement = (Announcement) abstractStatus; AbstractStatus originalStatus = statusRepository.findStatusById(announcement.getOriginalStatusId()); if (originalStatus == null) { log.debug("Original Status could not be found"); return details; } else if (originalStatus.getType() != null && !originalStatus.getType().equals(StatusType.STATUS)) { log.debug("Original status does not have the correct type"); return details; } status = (Status) originalStatus; } else { log.debug("Status does not have the correct type"); return details; } details.setStatusId(status.getStatusId()); // Shares management Collection sharedByLogins = sharesRepository.findLoginsWhoSharedAStatus(status.getStatusId()); details.setSharedByLogins(userService.getUsersByLogin(sharedByLogins)); log.debug("Status shared by {} users", sharedByLogins.size()); // Discussion management Collection statusIdsInDiscussion = new LinkedHashSet(); String replyTo = status.getReplyTo(); if (replyTo != null && !replyTo.equals("")) { // If this is a reply, get the original discussion // Add the original discussion statusIdsInDiscussion.add(status.getDiscussionId()); // Add the replies statusIdsInDiscussion.addAll(discussionRepository.findStatusIdsInDiscussion(status.getDiscussionId())); // Remove the current status from the list statusIdsInDiscussion.remove(status.getStatusId()); } else { // This is the original discussion // Add the replies statusIdsInDiscussion.addAll(discussionRepository.findStatusIdsInDiscussion(status.getStatusId())); } // Transform the Set to a Map List line = new ArrayList(); for (String statusIdInDiscussion : statusIdsInDiscussion) { line.add(statusIdInDiscussion); } // Enrich the details object with the complete statuses in the discussion Collection statusesInDiscussion = buildStatusList(line); details.setDiscussionStatuses(statusesInDiscussion); return details; } public Collection buildStatusList(List line) { User currentUser = null; Collection usergroups; List favoriteLine; if (authenticationService.hasAuthenticatedUser()) { currentUser = authenticationService.getCurrentUser(); usergroups = groupService.getGroupsForUser(currentUser); favoriteLine = favoritelineRepository.getFavoriteline(currentUser.getLogin()); } else { usergroups = Collections.emptyList(); favoriteLine = Collections.emptyList(); } Collection statuses = new ArrayList(line.size()); for (String statusId : line) { AbstractStatus abstractStatus = statusRepository.findStatusById(statusId); if (abstractStatus != null) { User statusUser = userService.getUserByLogin(abstractStatus.getLogin()); if (statusUser != null) { // Security check // bypass the security check when no user is logged in // => for non-authenticated rss access if ((currentUser != null) && !statusUser.getDomain().equals(currentUser.getDomain())) { throw new DomainViolationException("User " + currentUser + " tried to access " + " status : " + abstractStatus); } StatusDTO statusDTO = new StatusDTO(); statusDTO.setStatusId(abstractStatus.getStatusId()); statusDTO.setStatusDate(abstractStatus.getStatusDate()); statusDTO.setGeoLocalization(abstractStatus.getGeoLocalization()); statusDTO.setActivated(statusUser.getActivated()); StatusType type = abstractStatus.getType(); if (type == null) { statusDTO.setType(StatusType.STATUS); } else { statusDTO.setType(abstractStatus.getType()); } if (abstractStatus.getType().equals(StatusType.SHARE)) { Share share = (Share) abstractStatus; AbstractStatus originalStatus = statusRepository.findStatusById(share.getOriginalStatusId()); if (originalStatus != null) { // Find the original status statusDTO.setTimelineId(share.getStatusId()); statusDTO.setSharedByUsername(share.getUsername()); statusUser = userService.getUserByLogin(originalStatus.getLogin()); addStatusToLine(statuses, statusDTO, originalStatus, statusUser, usergroups, favoriteLine); } else { log.debug("Original status has been deleted"); } } else if (abstractStatus.getType().equals(StatusType.MENTION_SHARE)) { MentionShare mentionShare = (MentionShare) abstractStatus; AbstractStatus originalStatus = statusRepository.findStatusById(mentionShare.getOriginalStatusId()); if (originalStatus != null) { // Find the status that was shared statusDTO.setTimelineId(mentionShare.getStatusId()); statusDTO.setSharedByUsername(mentionShare.getUsername()); statusUser = userService.getUserByLogin(mentionShare.getLogin()); addStatusToLine(statuses, statusDTO, originalStatus, statusUser, usergroups, favoriteLine); } else { log.debug("Mentioned status has been deleted"); } } else if (abstractStatus.getType().equals(StatusType.MENTION_FRIEND)) { MentionFriend mentionFriend = (MentionFriend) abstractStatus; statusDTO.setTimelineId(mentionFriend.getStatusId()); //statusDTO.setSharedByUsername(mentionFriend.getUsername()); statusUser = userService.getUserByLogin(mentionFriend.getFollowerLogin()); statusDTO.setFirstName(statusUser.getFirstName()); statusDTO.setLastName(statusUser.getLastName()); statusDTO.setAvatar(statusUser.getAvatar()); statusDTO.setUsername(statusUser.getUsername()); statuses.add(statusDTO); } else if (abstractStatus.getType().equals(StatusType.ANNOUNCEMENT)) { Announcement announcement = (Announcement) abstractStatus; AbstractStatus originalStatus = statusRepository.findStatusById(announcement.getOriginalStatusId()); if (originalStatus != null) { // Find the status that was announced statusDTO.setTimelineId(announcement.getStatusId()); statusDTO.setSharedByUsername(announcement.getUsername()); statusUser = userService.getUserByLogin(originalStatus.getLogin()); addStatusToLine(statuses, statusDTO, originalStatus, statusUser, usergroups, favoriteLine); } else { log.debug("Announced status has been deleted"); } } else { // Normal status statusDTO.setTimelineId(abstractStatus.getStatusId()); addStatusToLine(statuses, statusDTO, abstractStatus, statusUser, usergroups, favoriteLine); } } else { log.debug("Deleted user : {}", abstractStatus.getLogin()); } } else { log.debug("Deleted status : {}", statusId); } } for(StatusDTO statusDTO : statuses) statusDTO.setShareByMe(shareByMe(statusDTO)); return statuses; } //@Cacheable("isSharedByMe") private Boolean shareByMe(StatusDTO statusDTO) { Boolean isSharedByMe; Collection loginWhoShare = sharesRepository.findLoginsWhoSharedAStatus(statusDTO.getStatusId()); User currentUser = authenticationService.getCurrentUser(); if(loginWhoShare.contains(currentUser.getLogin()) ) isSharedByMe = true; else if(currentUser.getUsername().equals(statusDTO.getSharedByUsername())) //Greg ce n'est pas normal de devoir faire ça isSharedByMe = true; else isSharedByMe = false; return isSharedByMe; } /** * Find old statuses. * * Statuses might not be up to date on the current line : they might have been deleted, or the user has lost the * permission to see them. */ private Collection findStatusesToCleanUp(List statuses, Collection dtos) { Collection statusIdsToCleanUp = new ArrayList(); for (String statusId : statuses) { boolean statusToDelete = true; for (StatusDTO statusDTO : dtos) { if (statusDTO.getStatusId().equals(statusId)) { statusToDelete = false; } } if (statusToDelete) { statusIdsToCleanUp.add(statusId); } } return statusIdsToCleanUp; } private void addStatusToLine(Collection line, StatusDTO statusDTO, AbstractStatus abstractStatus, User statusUser, Collection usergroups, List favoriteLine) { Status status = (Status) abstractStatus; // Group check boolean hiddenStatus = false; if (status.getGroupId() != null) { statusDTO.setGroupId(status.getGroupId()); Group group = groupService.getGroupById(statusUser.getDomain(), statusDTO.getGroupId()); // if this is a private group and the user is not part of it, he cannot see the status if (!group.isPublicGroup() && !usergroups.contains(group)) { hiddenStatus = true; } else { statusDTO.setPublicGroup(group.isPublicGroup()); statusDTO.setGroupName(group.getName()); } } if (!hiddenStatus) { if (status.getHasAttachments() != null && status.getHasAttachments()) { statusDTO.setAttachments(status.getAttachments()); } statusDTO.setContent(status.getContent()); statusDTO.setUsername(statusUser.getUsername()); if (status.getStatusPrivate() == null) { statusDTO.setStatusPrivate(false); } else { statusDTO.setStatusPrivate(status.getStatusPrivate()); } statusDTO.setReplyTo(status.getReplyTo()); statusDTO.setReplyToUsername(status.getReplyToUsername()); if (favoriteLine.contains(statusDTO.getStatusId())) { statusDTO.setFavorite(true); } else { statusDTO.setFavorite(false); } statusDTO.setFirstName(statusUser.getFirstName()); statusDTO.setLastName(statusUser.getLastName()); statusDTO.setAvatar(statusUser.getAvatar()); statusDTO.setDetailsAvailable(status.isDetailsAvailable()); line.add(statusDTO); } } /** * The mentionline contains a statuses where the current user is mentioned. * * @return a status list */ public Collection getMentionline(int nbStatus, String start, String finish) { User currentUser = authenticationService.getCurrentUser(); List statuses = mentionlineRepository.getMentionline(currentUser.getLogin(), nbStatus, start, finish); Collection dtos = buildStatusList(statuses); if (statuses.size() != dtos.size()) { Collection statusIdsToDelete = findStatusesToCleanUp(statuses, dtos); mentionlineRepository.removeStatusesFromMentionline(currentUser.getLogin(), statusIdsToDelete); return getMentionline(nbStatus, start, finish); } dtos = filterStatusFromBlockedUsers(dtos); return dtos; } /** * The tagline contains a tag's statuses * * @param tag the tag to retrieve the timeline of * @param nbStatus the number of status to retrieve, starting from most recent ones * @return a status list */ public Collection getTagline(String tag, int nbStatus, String start, String finish) { if (tag == null || tag.isEmpty()) { tag = hashtagDefault; } User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); List statuses = taglineRepository.getTagline(domain, tag, nbStatus, start, finish); Collection dtos = buildStatusList(statuses); if (statuses.size() != dtos.size()) { Collection statusIdsToDelete = findStatusesToCleanUp(statuses, dtos); taglineRepository.removeStatusesFromTagline(tag, domain, statusIdsToDelete); return getTagline(tag, nbStatus, start, finish); } dtos = filterStatusFromBlockedUsers(dtos); return dtos; } /** * The groupline contains a group's statuses. * * @return a status list */ public Collection getGroupline(String groupId, Integer nbStatus, String start, String finish) { List statuses = grouplineRepository.getGroupline(groupId, nbStatus, start, finish); Collection dtos = buildStatusList(statuses); if (statuses.size() != dtos.size()) { Collection statusIdsToDelete = findStatusesToCleanUp(statuses, dtos); grouplineRepository.removeStatusesFromGroupline(groupId, statusIdsToDelete); return getGroupline(groupId, nbStatus, start, finish); } dtos = filterStatusFromBlockedUsers(dtos); return dtos; } /** * The timeline contains the user's status merged with his friends status * * @param nbStatus the number of status to retrieve, starting from most recent ones * @return a status list */ public Collection getTimeline(int nbStatus, String start, String finish) { String login = authenticationService.getCurrentUser().getLogin(); return getUserTimeline(login, nbStatus, start, finish); } /** * The timeline contains the user's status merged with his friends status. * * getUserTimeline returns the time line for an arbitrary user (and not only * the logged-in user). * * This is used for RSS syndication. * * @param login of the user we want the timeline of * @param nbStatus the number of status to retrieve, starting from most recent ones * @return a status list */ public Collection getUserTimeline(String login, int nbStatus, String start, String finish) { List statuses = timelineRepository.getTimeline(login, nbStatus, start, finish); Collection dtos = buildStatusList(statuses); if (statuses.size() != dtos.size()) { Collection statusIdsToDelete = findStatusesToCleanUp(statuses, dtos); timelineRepository.removeStatusesFromTimeline(login, statusIdsToDelete); return getTimeline(nbStatus, start, finish); } dtos = filterStatusFromBlockedUsers(dtos); return dtos; } /** * The domainline contains all the public statuses of the domain (status with no group, or * in a public group), for the last 30 days. * * @param nbStatus the number of status to retrieve, starting from most recent ones * @return a status list */ public Collection getDomainline(int nbStatus, String start, String finish) { User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); List statuses = domainlineRepository.getDomainline(domain, nbStatus, start, finish); Collection dtos = buildStatusList(statuses); if (statuses.size() != dtos.size()) { Collection statusIdsToDelete = findStatusesToCleanUp(statuses, dtos); domainlineRepository.removeStatusFromDomainline(domain, statusIdsToDelete); return getDomainline(nbStatus, start, finish); } dtos = filterStatusFromBlockedUsers(dtos); return dtos; } /** * The userline contains the user's own status * * @param username the user to retrieve the userline of * @param nbStatus the number of status to retrieve, starting from most recent ones * @return a status list */ public Collection getUserline(String username, int nbStatus, String start, String finish) { String login; User currentUser = authenticationService.getCurrentUser(); if (username == null || username.isEmpty()) { // current user login = currentUser.getLogin(); } else { // another user, in the same domain String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); login = DomainUtil.getLoginFromUsernameAndDomain(username, domain); } List statuses = userlineRepository.getUserline(login, nbStatus, start, finish); Collection dtos = buildStatusList(statuses); if (statuses.size() != dtos.size()) { Collection statusIdsToDelete = findStatusesToCleanUp(statuses, dtos); userlineRepository.removeStatusesFromUserline(login, statusIdsToDelete); return getUserline(username, nbStatus, start, finish); } dtos = filterStatusFromBlockedUsers(dtos); return dtos; } public void removeStatus(String statusId) { log.debug("Removing status : {}", statusId); AbstractStatus abstractStatus = statusRepository.findStatusById(statusId); if (abstractStatus != null && abstractStatus.getType().equals(StatusType.STATUS)) { Status status = (Status) abstractStatus; User currentUser = authenticationService.getCurrentUser(); if (status.getLogin().equals(currentUser.getLogin()) || authenticationService.isCurrentUserInRole("ROLE_ADMIN")) { statusRepository.removeStatus(status); counterRepository.decrementStatusCounter(currentUser.getLogin()); searchService.removeStatus(status); } } else if (abstractStatus.getType().equals(StatusType.ANNOUNCEMENT)) { User currentUser = authenticationService.getCurrentUser(); if (abstractStatus.getLogin().equals(currentUser.getLogin())) { statusRepository.removeStatus(abstractStatus); } } else if(abstractStatus.getType().equals(StatusType.SHARE) && authenticationService.isCurrentUserInRole("ROLE_ADMIN")) { Share currentShare = (Share) abstractStatus; // We delete the original status String originalStatusId = currentShare.getOriginalStatusId(); removeStatus(originalStatusId); } else { log.debug("Cannot remove status of this type"); } } public void shareStatus(String statusId) { log.debug("Share status : {}", statusId); String currentLogin = this.authenticationService.getCurrentUser().getLogin(); AbstractStatus abstractStatus = statusRepository.findStatusById(statusId); if (abstractStatus != null) { if (abstractStatus.getType().equals(StatusType.STATUS)) { Status status = (Status) abstractStatus; internalShareStatus(currentLogin, status); } else if (abstractStatus.getType().equals(StatusType.SHARE)) { Share currentShare = (Share) abstractStatus; // We share the original status Status originalStatus = (Status) statusRepository.findStatusById(currentShare.getOriginalStatusId()); internalShareStatus(currentLogin, originalStatus); } else { log.warn("Cannot share this type of status: " + abstractStatus); } } else { log.debug("Cannot share this status, as it does not exist: {}", abstractStatus); } } private void internalShareStatus(String currentLogin, Status status) { // create share Share share = statusRepository.createShare(currentLogin, status.getStatusId()); // add status to the user's userline and timeline userlineRepository.shareStatusToUserline(currentLogin, share); shareStatusToTimelineAndNotify(currentLogin, currentLogin, share); // add status to the follower's timelines Collection followersForUser = followerRepository.findFollowersForUser(currentLogin); for (String followerLogin : followersForUser) { shareStatusToTimelineAndNotify(currentLogin, followerLogin, share); } // update the status details to add this share sharesRepository.newShareByLogin(status.getStatusId(), currentLogin); // mention the status' author that the user has shared his status MentionShare mentionShare = statusRepository.createMentionShare(currentLogin, status.getStatusId()); mentionlineRepository.addStatusToMentionline(status.getLogin(), mentionShare.getStatusId()); } public void addFavoriteStatus(String statusId) { log.debug("Favorite status : {}", statusId); AbstractStatus abstractStatus = statusRepository.findStatusById(statusId); if (abstractStatus.getType().equals(StatusType.STATUS)) { String login = authenticationService.getCurrentUser().getLogin(); favoritelineRepository.addStatusToFavoriteline(login, statusId); } else if(abstractStatus.getType().equals(StatusType.SHARE)){ Share currentShare = (Share) abstractStatus; // We add the original status the favorites String login = authenticationService.getCurrentUser().getLogin(); favoritelineRepository.addStatusToFavoriteline(login, currentShare.getOriginalStatusId()); } else { log.warn("Cannot favorite this type of status: " + abstractStatus); } } public void removeFavoriteStatus(String statusId) { log.debug("Un-favorite status : {}", statusId); AbstractStatus abstractStatus = statusRepository.findStatusById(statusId); if (abstractStatus.getType().equals(StatusType.STATUS)) { User currentUser = authenticationService.getCurrentUser(); favoritelineRepository.removeStatusFromFavoriteline(currentUser.getLogin(), statusId); } else { log.warn("Cannot un-favorite this type of status: " + abstractStatus); } } public void announceStatus(String statusId) { log.debug("Announce status : {}", statusId); String currentLogin = this.authenticationService.getCurrentUser().getLogin(); AbstractStatus abstractStatus = statusRepository.findStatusById(statusId); if (abstractStatus != null) { if (abstractStatus.getType().equals(StatusType.STATUS)) { Status status = (Status) abstractStatus; internalAnnounceStatus(currentLogin, status); } else if (abstractStatus.getType().equals(StatusType.SHARE)) { Share currentShare = (Share) abstractStatus; // We announce the original status Status originalStatus = (Status) statusRepository.findStatusById(currentShare.getOriginalStatusId()); internalAnnounceStatus(currentLogin, originalStatus); } else { log.warn("Cannot announce this type of status: " + abstractStatus); } } else { log.debug("Cannot announce this status, as it does not exist: {}", abstractStatus); } } private void internalAnnounceStatus(String currentLogin, Status status) { // create announcement Announcement announcement = statusRepository.createAnnouncement(currentLogin, status.getStatusId()); // add status to everyone's timeline String domain = DomainUtil.getDomainFromLogin(currentLogin); List logins = domainRepository.getLoginsInDomain(domain); timelineRepository.announceStatusToTimeline(currentLogin, logins, announcement); for (String login : logins) { atmosphereService.notifyUser(login, announcement); } } /** * The favline contains the user's favorites status * * @return a status list */ public Collection getFavoritesline() { String currentLogin = authenticationService.getCurrentUser().getLogin(); List statuses = favoritelineRepository.getFavoriteline(currentLogin); Collection dtos = buildStatusList(statuses); if (statuses.size() != dtos.size()) { Collection statusIdsToDelete = findStatusesToCleanUp(statuses, dtos); for (String statusId : statusIdsToDelete) { favoritelineRepository.removeStatusFromFavoriteline(currentLogin, statusId); } return getFavoritesline(); } dtos = filterStatusFromBlockedUsers(dtos); return dtos; } /** * Adds the status to the timeline and notifies the user with Atmosphere. */ private void shareStatusToTimelineAndNotify(String sharedByLogin, String timelineLogin, Share share) { timelineRepository.shareStatusToTimeline(sharedByLogin, timelineLogin, share); atmosphereService.notifyUser(timelineLogin, share); } private Collection filterStatusFromBlockedUsers(Collection dtos){ if(authenticationService.hasAuthenticatedUser()) { User currentUser = authenticationService.getCurrentUser(); String currentLogin = currentUser.getLogin(); String domain = DomainUtil.getDomainFromLogin(currentLogin); Collection blockedUsers = blockService.getUsersBlockedLoginForUser(currentLogin); Collection newDtos = new ArrayList(); for (StatusDTO dto : dtos) { if (!blockedUsers.contains(dto.getUsername() + "@" + domain)) { newDtos.add(dto); } } return newDtos; } return dtos; } public void hideStatus(String statusId) { log.debug("Hiding status from timeline : {}", statusId); User currentUser = authenticationService.getCurrentUser(); String login = currentUser.getLogin(); Collection statusIds = new ArrayList(); statusIds.add(statusId); timelineRepository.removeStatusesFromTimeline(login,statusIds); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/TrendService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.repository.TrendRepository; import fr.ippon.tatami.repository.UserTrendRepository; import fr.ippon.tatami.web.rest.dto.Trend; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import static fr.ippon.tatami.service.util.AnalysisUtil.findMostUsedKeys; import static fr.ippon.tatami.service.util.AnalysisUtil.incrementKeyCounterInMap; /** * Analyzes trends (tags going up or down depending on the current time). */ @Service public class TrendService { private final Logger log = LoggerFactory.getLogger(TrendService.class); private static final int TRENDS_SIZE = 8; @Inject private TrendRepository trendRepository; @Inject private UserTrendRepository userTrendRepository; @Cacheable("trends-cache") public List getCurrentTrends(String domain) { List tags = trendRepository.getRecentTags(domain); return calculateTrends(tags); } public Collection searchTags(String domain, String startWith, int size) { Assert.hasLength(startWith); Collection allTags = trendRepository.getDomainTags(domain); Collection matchingTags = new ArrayList(); String startWithLowered = startWith.toLowerCase(); int counter = 0; for (String tag : allTags) { if (tag.toLowerCase().startsWith(startWithLowered)) { matchingTags.add(tag); counter++; } if (counter == size) { break; } } return matchingTags; } @Cacheable("user-trends-cache") public List getTrendsForUser(String login) { List tags = userTrendRepository.getRecentTags(login); return calculateTrends(tags); } private List calculateTrends(List tags) { log.debug("All tags: {}", tags); HashMap totalTagsCount = new HashMap(); HashMap recentTagsCount = new HashMap(); HashMap oldTagsCount = new HashMap(); int currentPosition = 0; int middlePosition = tags.size() / 2; for (String tag : tags) { incrementKeyCounterInMap(totalTagsCount, tag); if (currentPosition <= middlePosition) { incrementKeyCounterInMap(recentTagsCount, tag); } else { incrementKeyCounterInMap(oldTagsCount, tag); } currentPosition++; } List mostUsedTags = findMostUsedKeys(totalTagsCount); List trends = new ArrayList(); for (String tag : mostUsedTags) { Trend trend = new Trend(); trend.setTag(tag); Integer recentCount = recentTagsCount.get(tag); Integer oldCount = oldTagsCount.get(tag); if (oldCount != null) { if (recentCount != null) { if (recentCount >= oldCount) { trend.setTrendingUp(true); } else { trend.setTrendingUp(false); } } else { trend.setTrendingUp(false); } } else { trend.setTrendingUp(true); } trends.add(trend); } if (trends.size() > TRENDS_SIZE) { return trends.subList(0, TRENDS_SIZE); } else { return trends; } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/UserService.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.domain.DigestType; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.*; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.dto.UserDTO; import fr.ippon.tatami.service.util.DomainUtil; import fr.ippon.tatami.service.util.RandomUtil; import org.springframework.security.access.annotation.Secured; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.security.crypto.password.StandardPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.cache.annotation.CacheEvict; import javax.inject.Inject; import javax.validation.ConstraintViolationException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.List; /** * Manages the application's users. * * @author Julien Dubois */ @Service public class UserService { private final Logger log = LoggerFactory.getLogger(UserService.class); @Inject private UserRepository userRepository; @Inject private DomainRepository domainRepository; @Inject private FriendshipService friendshipService; @Inject private FriendRepository friendRepository; @Inject private BlockService blockService; @Inject private FollowerRepository followerRepository; @Inject private CounterRepository counterRepository; @Inject private FavoritelineRepository favoritelineRepository; @Inject private AuthenticationService authenticationService; @Inject private MailService mailService; @Inject private TimelineRepository timelineRepository; @Inject private UserlineRepository userlineRepository; @Inject private RegistrationRepository registrationRepository; @Inject private RssUidRepository rssUidRepository; @Inject private SearchService searchService; @Inject private MailDigestRepository mailDigestRepository; @Inject Environment env; public User getUserByLogin(String login) { return userRepository.findUserByLogin(login); } public String getLoginByRssUid(String rssUid) { return rssUidRepository.getLoginByRssUid(rssUid); } public User getUserByUsername(String username) { User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); String login = DomainUtil.getLoginFromUsernameAndDomain(username, domain); return getUserByLogin(login); } /** * Return a collection of Users based on their username (ie : uid) * * @param logins the collection : must not be null * @return a Collection of User */ public Collection getUsersByLogin(Collection logins) { final Collection users = new ArrayList(); User user; for (String login : logins) { user = userRepository.findUserByLogin(login); if (user != null) { users.add(user); } } return users; } public List getUsersForCurrentDomain(int pagination) { User currentUSer = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUSer.getLogin()); List logins = domainRepository.getLoginsInDomain(domain, pagination); List users = new ArrayList(); for (String login : logins) { User user = getUserByLogin(login); users.add(user); } return users; } public void updateUser(User user) { User currentUser = authenticationService.getCurrentUser(); user.setLogin(currentUser.getLogin()); user.setUsername(currentUser.getUsername()); user.setDomain(currentUser.getDomain()); user.setAvatar(currentUser.getAvatar()); user.setAttachmentsSize(currentUser.getAttachmentsSize()); try { userRepository.updateUser(user); searchService.removeUser(user); searchService.addUser(user); } catch (ConstraintViolationException cve) { log.info("Constraint violated while updating user " + user + " : " + cve); throw cve; } } public void updatePassword(User user) { User currentUser = authenticationService.getCurrentUser(); String password = user.getPassword(); StandardPasswordEncoder encoder = new StandardPasswordEncoder(); String encryptedPassword = encoder.encode(password); currentUser.setPassword(encryptedPassword); log.debug("Password encrypted to : {}", encryptedPassword); try { userRepository.updateUser(currentUser); } catch (ConstraintViolationException cve) { log.info("Constraint violated while updating user " + user + " : " + cve); throw cve; } } public void createUser(User user) { String login = user.getLogin(); String username = DomainUtil.getUsernameFromLogin(login); String domain = DomainUtil.getDomainFromLogin(login); domainRepository.addUserInDomain(domain, login); // If the user is using OpenID or LDAP, the password is not set. // In this case, we generate a random password as Spring Security requires the user // to have a non-null password (and it is of course a better security than no password) if (user.getPassword() == null) { String password = RandomUtil.generatePassword(); StandardPasswordEncoder encoder = new StandardPasswordEncoder(); String encryptedPassword = encoder.encode(password); user.setPassword(encryptedPassword); } user.setUsername(username); user.setDomain(domain); user.setFirstName(StringUtils.defaultString(user.getFirstName())); user.setLastName(StringUtils.defaultString(user.getLastName())); user.setJobTitle(""); user.setAvatar(""); user.setPhoneNumber(""); user.setPreferencesMentionEmail(true); user.setWeeklyDigestSubscription(true); counterRepository.createStatusCounter(user.getLogin()); counterRepository.createFriendsCounter(user.getLogin()); counterRepository.createFollowersCounter(user.getLogin()); userRepository.createUser(user); // Add to the searchStatus engine searchService.addUser(user); log.debug("Created User : {}", user.toString()); } public void createTatamibot(String domain) { String login = DomainUtil.getLoginFromUsernameAndDomain(Constants.TATAMIBOT_NAME, domain); User tatamiBotUser = new User(); tatamiBotUser.setLogin(login); this.createUser(tatamiBotUser); tatamiBotUser.setPreferencesMentionEmail(false); tatamiBotUser.setWeeklyDigestSubscription(false); tatamiBotUser.setJobTitle("I am just a robot"); userRepository.updateUser(tatamiBotUser); log.debug("Created Tatami Bot user for domain : {}", domain); } public void deleteUser(User user) { // Unfollow this user Collection followersIds = friendshipService.getFollowerIdsForUser(user.getLogin()); for (String followerId : followersIds) { User follower = getUserByLogin(followerId); friendshipService.unfollowUser(follower, user); } log.debug("Delete user step 1 : Unfollowed user " + user.getLogin()); // Unfollow friends Collection friendsIds = friendshipService.getFriendIdsForUser(user.getLogin()); for (String friendId : friendsIds) { User friend = getUserByLogin(friendId); friendshipService.unfollowUser(user, friend); } log.debug("Delete user step 2 : user " + user.getLogin() + " has no more friends."); // Delete userline, tagLine... favoritelineRepository.deleteFavoriteline(user.getLogin()); timelineRepository.deleteTimeline(user.getLogin()); userlineRepository.deleteUserline(user.getLogin()); log.debug("Delete user step 3 : user " + user.getLogin() + " has no more lines."); // Remove from domain String domain = DomainUtil.getDomainFromLogin(user.getLogin()); domainRepository.deleteUserInDomain(domain, user.getLogin()); log.debug("Delete user step 4 : user " + user.getLogin() + " has no domain."); // Delete counters counterRepository.deleteCounters(user.getLogin()); log.debug("Delete user step 5 : user " + user.getLogin() + " has no counter."); // Delete user userRepository.deleteUser(user); log.debug("Delete user step 6 : user " + user.getLogin() + " is deleted."); // Tweets are not deleted, but are not available to users anymore (unless the same user is created again) log.debug("User " + user.getLogin() + "has been successfully deleted !"); } /** * Set activated Field to false. */ @Secured("ROLE_ADMIN") @CacheEvict(value = {"group-user-cache", "group-cache","suggest-users-cache"}, allEntries = true) public boolean desactivateUser( String username ) { User user = getUserByUsername(username); if ( user != null ) { // Desactivate/Activate User if ( user.getActivated() ) { userRepository.desactivateUser(user); favoritelineRepository.deleteFavoriteline(user.getLogin()); mailService.sendDeactivatedEmail(user.getLogin()); log.debug("User " + user.getLogin() + " has been successfully desactivated !"); } else { userRepository.reactivateUser(user); log.debug("User " + user.getLogin() + " has been successfully reactivated !"); } return true; } log.debug("User " + user.getLogin() + " NOT FOUND !"); return false; } /** * Creates a User and sends a registration e-mail. */ public void registerUser(User user) { String registrationKey = registrationRepository.generateRegistrationKey(user.getLogin()); mailService.sendRegistrationEmail(registrationKey, user); } public void lostPassword(User user) { String registrationKey = registrationRepository.generateRegistrationKey(user.getLogin()); mailService.sendLostPasswordEmail(registrationKey, user); } public String validateRegistration(String key) { log.debug("Validating registration for key {}", key); String login = registrationRepository.getLoginByRegistrationKey(key); String password = RandomUtil.generatePassword(); StandardPasswordEncoder encoder = new StandardPasswordEncoder(); String encryptedPassword = encoder.encode(password); if (login != null) { User existingUser = getUserByLogin(login); if (existingUser != null) { log.debug("Reinitializing password for user {}", login); existingUser.setPassword(encryptedPassword); userRepository.updateUser(existingUser); mailService.sendPasswordReinitializedEmail(existingUser, password); } else { log.debug("Validating user {}", login); User user = new User(); user.setLogin(login); user.setPassword(encryptedPassword); createUser(user); mailService.sendValidationEmail(user, password); } } return login; } /** * update registration to weekly digest email. */ public void updateWeeklyDigestRegistration(boolean registration) { User currentUser = authenticationService.getCurrentUser(); currentUser.setWeeklyDigestSubscription(registration); String day = String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)); if (registration) { mailDigestRepository.subscribeToDigest(DigestType.WEEKLY_DIGEST, currentUser.getLogin(), currentUser.getDomain(), day); } else { mailDigestRepository.unsubscribeFromDigest(DigestType.WEEKLY_DIGEST, currentUser.getLogin(), currentUser.getDomain(), day); } log.debug("Updating weekly digest preferences : " + "weeklyDigest={} for user {}", registration, currentUser.getLogin()); try { userRepository.updateUser(currentUser); } catch (ConstraintViolationException cve) { log.info("Constraint violated while updating preferences : " + cve); throw cve; } } /** * Update registration to daily digest email. */ public void updateDailyDigestRegistration(boolean registration) { User currentUser = authenticationService.getCurrentUser(); currentUser.setDailyDigestSubscription(registration); String day = String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)); if (registration) { mailDigestRepository.subscribeToDigest(DigestType.DAILY_DIGEST, currentUser.getLogin(), currentUser.getDomain(), day); } else { mailDigestRepository.unsubscribeFromDigest(DigestType.DAILY_DIGEST, currentUser.getLogin(), currentUser.getDomain(), day); } log.debug("Updating daily digest preferences : dailyDigest={} for user {}", registration, currentUser.getLogin()); try { userRepository.updateUser(currentUser); } catch (ConstraintViolationException cve) { log.info("Constraint violated while updating preferences : " + cve); throw cve; } } /** * Activate of de-activate rss publication for the timeline. * * @return the rssUid used for rss publication, empty if no publication */ public String updateRssTimelinePreferences(boolean booleanPreferencesRssTimeline) { User currentUser = authenticationService.getCurrentUser(); String rssUid = currentUser.getRssUid(); if (booleanPreferencesRssTimeline) { // if we already have an rssUid it means it's already activated : // nothing to do, we do not want to change it if ((rssUid == null) || rssUid.equals("")) { // Activate rss feed publication. rssUid = rssUidRepository.generateRssUid(currentUser.getLogin()); currentUser.setRssUid(rssUid); log.debug("Updating rss timeline preferences : rssUid={}", rssUid); try { userRepository.updateUser(currentUser); } catch (ConstraintViolationException cve) { log.info("Constraint violated while updating preferences : " + cve); throw cve; } } } else { // Remove current rssUid from both CF! if ((rssUid != null) && (!rssUid.isEmpty())) { rssUidRepository.removeRssUid(rssUid); rssUid = ""; currentUser.setRssUid(rssUid); log.debug("Updating rss timeline preferences : rssUid={}", rssUid); try { userRepository.updateUser(currentUser); } catch (ConstraintViolationException cve) { log.info("Constraint violated while updating preferences : " + cve); throw cve; } } } return rssUid; } /** * Is the domain managed by a LDAP repository? */ public boolean isDomainHandledByLDAP(String domain) { String domainHandledByLdap = env.getProperty("tatami.ldapauth.domain"); return domain.equalsIgnoreCase(domainHandledByLdap); } public Collection buildUserDTOList(Collection users) { User currentUser = authenticationService.getCurrentUser(); Collection currentFriendLogins = friendRepository.findFriendsForUser(currentUser.getLogin()); Collection currentFollowersLogins = followerRepository.findFollowersForUser(currentUser.getLogin()); Collection currentBlockedUsersLogins = blockService.getUsersBlockedLoginForUser(currentUser.getLogin()); Collection userDTOs = new ArrayList(); for (User user : users) { UserDTO userDTO = getUserDTOFromUser(user); userDTO.setYou(user.equals(currentUser)); if (!userDTO.isYou()) { userDTO.setFriend(currentFriendLogins.contains(user.getLogin())); userDTO.setFollower(currentFollowersLogins.contains(user.getLogin())); userDTO.setBlocked(currentBlockedUsersLogins.contains(user.getLogin())); } userDTOs.add(userDTO); } return userDTOs; } public UserDTO buildUserDTO(User user) { User currentUser = authenticationService.getCurrentUser(); UserDTO userDTO = getUserDTOFromUser(user); userDTO.setYou(user.equals(currentUser)); if (!userDTO.isYou()) { Collection currentFriendLogins = friendRepository.findFriendsForUser(currentUser.getLogin()); Collection currentFollowersLogins = followerRepository.findFollowersForUser(currentUser.getLogin()); Collection currentBlockedUsersLogins = blockService.getUsersBlockedLoginForUser(currentUser.getLogin()); userDTO.setFriend(currentFriendLogins.contains(user.getLogin())); userDTO.setFollower(currentFollowersLogins.contains(user.getLogin())); userDTO.setBlocked(currentBlockedUsersLogins.contains(user.getLogin())); } return userDTO; } private UserDTO getUserDTOFromUser(User user) { UserDTO friend = new UserDTO(); friend.setLogin(user.getLogin()); friend.setUsername(user.getUsername()); friend.setAvatar(user.getAvatar()); friend.setFirstName(user.getFirstName()); friend.setLastName(user.getLastName()); friend.setJobTitle(user.getJobTitle()); friend.setPhoneNumber(user.getPhoneNumber()); friend.setAttachmentsSize(user.getAttachmentsSize()); friend.setStatusCount(user.getStatusCount()); friend.setFriendsCount(user.getFriendsCount()); friend.setFollowersCount(user.getFollowersCount()); friend.setActivated(user.getActivated()); return friend; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/dto/StatusDTO.java ================================================ package fr.ippon.tatami.service.dto; import fr.ippon.tatami.domain.Attachment; import fr.ippon.tatami.domain.status.StatusType; import org.joda.time.DateTime; import org.joda.time.Period; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.DateTimeFormatterBuilder; import org.joda.time.format.ISODateTimeFormat; import java.io.Serializable; import java.util.Calendar; import java.util.Collection; import java.util.Date; /** * DTO to present a "complete" status to the presentation layer. */ public class StatusDTO implements Serializable { private static final DateTimeFormatter iso8601Formatter = ISODateTimeFormat.dateTime(); private static final DateTimeFormatter basicDateFormatter = new DateTimeFormatterBuilder() .appendDayOfMonth(1) .appendLiteral(' ') .appendMonthOfYearShortText() .toFormatter(); private static final DateTimeFormatter oldDateFormatter = new DateTimeFormatterBuilder() .appendDayOfMonth(1) .appendLiteral(' ') .appendMonthOfYearShortText() .appendLiteral(' ') .appendYear(4, 4) .toFormatter(); private String statusId; /** * The timelineId is used on the client side : * - When this is an original status, timelineId = statusId * - When this is a shared status, timelineId = the id of this share in the user's timeline */ private String timelineId; private StatusType type; private String username; private boolean statusPrivate; private boolean activated; private String groupId; private String groupName; private String geoLocalization; private boolean publicGroup; private Collection attachments; private Collection attachmentIds; private String content; private Date statusDate; private String iso8601StatusDate; private String prettyPrintStatusDate; /** * If this status is a reply, the statusId of the original status. */ private String replyTo; /** * If this status is a reply, the username who posted the original status. */ private String replyToUsername; private String firstName; private String lastName; private String avatar; private boolean favorite; private boolean detailsAvailable; /** * If this status was shared, username of the user who shared it. */ private String sharedByUsername; private boolean shareByMe; public boolean isActivated() { return activated; } public void setActivated(boolean activated) { this.activated = activated; } public boolean isShareByMe() { return shareByMe; } public void setShareByMe(boolean shareByMe) { this.shareByMe = shareByMe; } public String getISO8601StatusDate() { return this.iso8601StatusDate; } public String getPrettyPrintStatusDate() { return this.prettyPrintStatusDate; } public String getStatusId() { return statusId; } public void setStatusId(String statusId) { this.statusId = statusId; } public String getTimelineId() { return timelineId; } public void setTimelineId(String timelineId) { this.timelineId = timelineId; } public StatusType getType() { return type; } public void setType(StatusType type) { this.type = type; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public boolean isStatusPrivate() { return statusPrivate; } public void setStatusPrivate(boolean statusPrivate) { this.statusPrivate = statusPrivate; } public String getGroupId() { return groupId; } public void setGroupId(String groupId) { this.groupId = groupId; } public String getGroupName() { return groupName; } public void setGroupName(String groupName) { this.groupName = groupName; } public boolean isPublicGroup() { return publicGroup; } public void setPublicGroup(boolean publicGroup) { this.publicGroup = publicGroup; } public Collection getAttachmentIds() { return attachmentIds; } public void setAttachmentIds(Collection attachmentIds) { this.attachmentIds = attachmentIds; } public Collection getAttachments() { return attachments; } public void setAttachments(Collection attachments) { this.attachments = attachments; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public Date getStatusDate() { return statusDate; } public void setStatusDate(Date statusDate) { this.statusDate = statusDate; if (statusDate != null) { DateTime dateTime = new DateTime(statusDate); Period period = new Period(statusDate.getTime(), Calendar.getInstance().getTimeInMillis()); if (period.getMonths() < 1) { // Only format if it is more than 1 month old this.iso8601StatusDate = iso8601Formatter.print(dateTime); } else { this.iso8601StatusDate = ""; } if (period.getYears() == 0) { // Only print the year if it is more than 1 year old this.prettyPrintStatusDate = basicDateFormatter.print(dateTime); } else { this.prettyPrintStatusDate = oldDateFormatter.print(dateTime); } } else { this.iso8601StatusDate = ""; this.prettyPrintStatusDate = ""; } } public String getReplyTo() { return replyTo; } public void setReplyTo(String replyTo) { this.replyTo = replyTo; } public String getReplyToUsername() { return replyToUsername; } public void setReplyToUsername(String replyToUsername) { this.replyToUsername = replyToUsername; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public boolean isFavorite() { return favorite; } public void setFavorite(boolean favorite) { this.favorite = favorite; } public boolean isDetailsAvailable() { return detailsAvailable; } public void setDetailsAvailable(boolean detailsAvailable) { this.detailsAvailable = detailsAvailable; } public String getSharedByUsername() { return sharedByUsername; } public void setSharedByUsername(String sharedByUsername) { this.sharedByUsername = sharedByUsername; } public String getGeoLocalization() { return geoLocalization; } public void setGeoLocalization(String geoLocalization) { this.geoLocalization = geoLocalization; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; StatusDTO status = (StatusDTO) o; return !(statusId != null ? !statusId.equals(status.statusId) : status.statusId != null); } @Override public int hashCode() { return statusId != null ? statusId.hashCode() : 0; } @Override public String toString() { return "StatusDTO{" + "statusId='" + statusId + '\'' + ", timelineId='" + timelineId + '\'' + ", type=" + type + ", username='" + username + '\'' + ", statusPrivate=" + statusPrivate + ", groupId='" + groupId + '\'' + ", groupName='" + groupName + '\'' + ", geoLocalization='" + geoLocalization + '\'' + ", publicGroup=" + publicGroup + ", attachments=" + attachments + ", attachmentIds=" + attachmentIds + ", content='" + content + '\'' + ", statusDate=" + statusDate + ", iso8601StatusDate='" + iso8601StatusDate + '\'' + ", prettyPrintStatusDate='" + prettyPrintStatusDate + '\'' + ", replyTo='" + replyTo + '\'' + ", replyToUsername='" + replyToUsername + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", avatar='" + avatar + '\'' + ", favorite=" + favorite + ", detailsAvailable=" + detailsAvailable + ", sharedByUsername='" + sharedByUsername + '\'' + ", shareByMe='" + shareByMe + '\'' + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/dto/UserDTO.java ================================================ package fr.ippon.tatami.service.dto; import java.io.Serializable; /** * DTO to present a "complete" status to the presentation layer. */ public class UserDTO implements Serializable { private String login; private String username; private String avatar; private String firstName; private String lastName; private String jobTitle; private String phoneNumber; private long attachmentsSize; private long statusCount; private long friendsCount; private long followersCount; private boolean isFriend = false; private boolean isFollower = false; private boolean isYou = false; private boolean isActivated=true; private boolean isBlocked = false; private boolean isAdmin = false; public boolean isActivated() { return isActivated; } public void setActivated(boolean activated) { this.isActivated = activated; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getJobTitle() { return jobTitle; } public void setJobTitle(String jobTitle) { this.jobTitle = jobTitle; } public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } public long getAttachmentsSize() { return attachmentsSize; } public void setAttachmentsSize(long attachmentsSize) { this.attachmentsSize = attachmentsSize; } public long getStatusCount() { return statusCount; } public void setStatusCount(long statusCount) { this.statusCount = statusCount; } public long getFriendsCount() { return friendsCount; } public void setFriendsCount(long friendsCount) { this.friendsCount = friendsCount; } public long getFollowersCount() { return followersCount; } public void setFollowersCount(long followersCount) { this.followersCount = followersCount; } public boolean isFriend() { return isFriend; } public void setFriend(boolean friend) { isFriend = friend; } public boolean isFollower() { return isFollower; } public void setFollower(boolean follower) { isFollower = follower; } public boolean isYou() { return isYou; } public void setYou(boolean you) { isYou = you; } public boolean isBlocked() { return isBlocked; } public void setBlocked(boolean blocked) { isBlocked = blocked; } public boolean getIsAdmin() { return isAdmin; } public void setIsAdmin(boolean isAdmin) { this.isAdmin = isAdmin; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserDTO user = (UserDTO) o; return !(username != null ? !username.equals(user.username) : user.username != null); } @Override public int hashCode() { return username != null ? username.hashCode() : 0; } @Override public String toString() { return "UserDTO{" + "username='" + username + '\'' + ", avatar='" + avatar + '\'' + ", login='" + login + '\'' + ", firstName='" + firstName + '\'' + ", lastName=" + lastName + '\'' + ", jobTitle='" + jobTitle + '\'' + ", phoneNumber='" + phoneNumber + '\'' + ", attachmentsSize=" + attachmentsSize + ", statusCount=" + statusCount + ", friendsCount=" + friendsCount + ", followersCount=" + followersCount + ", isFriend=" + isFriend + ", isFollower=" + isFollower + ", isYou=" + isYou + ", isBlocked=" + isBlocked + ", activated=" + isActivated + '}'; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/dto/UserGroupDTO.java ================================================ package fr.ippon.tatami.service.dto; import java.io.Serializable; /** * DTO to manage a user in a group. */ public class UserGroupDTO implements Comparable, Serializable { private String login; private String avatar; private String username; private String firstName; private String lastName; private String role; private Boolean isMember = true; private boolean friend; private boolean activated; private boolean you; public boolean isActivated() { return activated; } public void setActivated(boolean activated) { this.activated = activated; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public Boolean getIsMember() { return isMember; } public void setIsMember(Boolean isMember) { this.isMember = isMember; } public boolean isFriend() { return friend; } public void setFriend(boolean friend) { this.friend = friend; } public boolean isYou() { return you; } public void setYou(boolean you) { this.you = you; } @Override public int compareTo(UserGroupDTO o) { return this.username.compareTo(o.getUsername()); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserGroupDTO userGroupDTO = (UserGroupDTO) o; return login.equals(userGroupDTO.login); } @Override public int hashCode() { return login.hashCode(); } @Override public String toString() { return "UserGroupDTO{" + "login='" + login + '\'' + ", avatar='" + avatar + '\'' + ", username='" + username + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", isMember=" + isMember + ", role='" + role + '\'' + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/elasticsearch/ElasticsearchEngine.java ================================================ package fr.ippon.tatami.service.elasticsearch; import org.elasticsearch.client.Client; /** * Elasticsearch engine. */ public interface ElasticsearchEngine { /** * @return Elasticsearch client. */ Client client(); } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/elasticsearch/ElasticsearchSearchService.java ================================================ package fr.ippon.tatami.service.elasticsearch; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.Status; import fr.ippon.tatami.repository.GroupDetailsRepository; import fr.ippon.tatami.service.SearchService; import org.apache.commons.lang.StringUtils; import org.elasticsearch.ElasticSearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Client; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.indices.IndexMissingException; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.scheduling.annotation.Async; import org.springframework.util.Assert; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.io.IOException; import java.net.URL; import java.util.*; import static org.elasticsearch.index.query.FilterBuilders.termFilter; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; public class ElasticsearchSearchService implements SearchService { private static final Logger log = LoggerFactory.getLogger(ElasticsearchSearchService.class); private static final String ALL_FIELD = "_all"; private static final List TYPES = Collections.unmodifiableList(Arrays.asList("user", "status", "group")); @Inject private ElasticsearchEngine engine; @Inject private String indexNamePrefix; @Inject private GroupDetailsRepository groupDetailsRepository; private Client client() { return engine.client(); } private String indexName(String type) { return StringUtils.isEmpty(indexNamePrefix) ? type : indexNamePrefix + '-' + type; } @PostConstruct private void init() { for (String type : TYPES) { if (!client().admin().indices().prepareExists(indexName(type)).execute().actionGet().exists()) { log.info("Index {} does not exists in Elasticsearch, creating it!", indexName(type)); createIndex(); } } } @Override public boolean reset() { log.info("Reseting ElasticSearch Index"); if (deleteIndex()) { return createIndex(); } else { log.warn("ElasticSearch Index could not be reset!"); return false; } } /** * Delete the tatami index. * * @return {@code true} if the index is deleted or didn't exist. */ private boolean deleteIndex() { for (String type : TYPES) { try { boolean ack = client().admin().indices().prepareDelete(indexName(type)).execute().actionGet().acknowledged(); if (!ack) { log.error("Elasticsearch Index wasn't deleted !"); return false; } } catch (IndexMissingException e) { // Failling to delete a missing index is supposed to be valid log.warn("Elasticsearch Index " + indexName(type) + " missing, it was not deleted"); } catch (ElasticSearchException e) { log.error("Elasticsearch Index " + indexName(type) + " was not deleted", e); return false; } } log.debug("Elasticsearch Index deleted!"); return true; } /** * Create the tatami index. * * @return {@code true} if an error occurs during the creation. */ private boolean createIndex() { for (String type : TYPES) { try { CreateIndexRequestBuilder createIndex = client().admin().indices().prepareCreate(indexName(type)); URL mappingUrl = getClass().getClassLoader().getResource("META-INF/elasticsearch/index/" + type + ".json"); ObjectMapper jsonMapper = new ObjectMapper(); JsonNode indexConfig = jsonMapper.readTree(mappingUrl); JsonNode indexSettings = indexConfig.get("settings"); if (indexSettings != null && indexSettings.isObject()) { createIndex.setSettings(jsonMapper.writeValueAsString(indexSettings)); } JsonNode mappings = indexConfig.get("mappings"); if (mappings != null && mappings.isObject()) { for (Iterator> i = mappings.fields(); i.hasNext(); ) { Map.Entry field = i.next(); ObjectNode mapping = jsonMapper.createObjectNode(); mapping.put(field.getKey(), field.getValue()); createIndex.addMapping(field.getKey(), jsonMapper.writeValueAsString(mapping)); } } boolean ack = createIndex.execute().actionGet().acknowledged(); if (!ack) { log.error("Cannot create index " + indexName(type)); return false; } } catch (ElasticSearchException e) { log.error("Cannot create index " + indexName(type), e); return false; } catch (IOException e) { log.error("Cannot create index " + indexName(type), e); return false; } } return true; } private final ElasticsearchMapper statusMapper = new ElasticsearchMapper() { @Override public String id(Status status) { return status.getStatusId(); } @Override public String type() { return "status"; } @Override public String prefixSearchSortField() { return null; } @Override public XContentBuilder toJson(Status status) throws IOException { XContentBuilder source = XContentFactory.jsonBuilder() .startObject() .field("statusId", status.getStatusId()) .field("domain", status.getDomain()) .field("username", status.getUsername()) .field("statusDate", status.getStatusDate()) .field("content", status.getContent()); if (status.getGroupId() != null) { Group group = groupDetailsRepository.getGroupDetails(status.getGroupId()); source.field("groupId", status.getGroupId()); source.field("publicGroup", group.isPublicGroup()); } return source.endObject(); } }; @Override @Async public void addStatus(Status status) { index(status, statusMapper); } @Override public void addStatuses(Collection statuses) { indexAll(statuses, statusMapper); } @Override public void removeStatus(Status status) { Assert.notNull(status, "status cannot be null"); delete(status, statusMapper); } @Override public List searchStatus(final String domain, final String query, int page, int size) { Assert.notNull(query); Assert.notNull(domain); if (page < 0) { page = 0; //Default value } if (size <= 0) { size = SearchService.DEFAULT_PAGE_SIZE; } try { SearchRequestBuilder searchRequest = client().prepareSearch(indexName(statusMapper.type())) .setTypes(statusMapper.type()) .setQuery(matchQuery(ALL_FIELD, query)) .setFilter(termFilter("domain", domain)) .addFields() .setFrom(page * size) .setSize(size) .addSort("statusDate", SortOrder.DESC); if (log.isTraceEnabled()) { log.trace("elasticsearch query : " + searchRequest); } SearchResponse searchResponse = searchRequest.execute().actionGet(); SearchHits searchHits = searchResponse.hits(); Long hitsNumber = searchHits.totalHits(); if (hitsNumber == 0) { return Collections.emptyList(); } SearchHit[] hits = searchHits.hits(); List items = new ArrayList(hits.length); for (SearchHit hit : hits) { items.add(hit.getId()); } log.debug("search status with words ({}) = {}", query, items); return items; } catch (IndexMissingException e) { log.warn("The index " + indexName(statusMapper.type()) + " was not found in the Elasticsearch cluster."); return Collections.emptyList(); } catch (ElasticSearchException e) { log.error("Error happened while searching status in index " + indexName(statusMapper.type())); return Collections.emptyList(); } } private final ElasticsearchMapper userMapper = new ElasticsearchMapper() { @Override public String id(User user) { return user.getLogin(); } @Override public String type() { return "user"; } @Override public String prefixSearchSortField() { return "username"; } @Override public XContentBuilder toJson(User user) throws IOException { return XContentFactory.jsonBuilder() .startObject() .field("login", user.getLogin()) .field("domain", user.getDomain()) .field("username", user.getUsername()) .field("firstName", user.getFirstName()) .field("lastName", user.getLastName()) .endObject(); } }; @Override @Async public void addUser(final User user) { Assert.notNull(user, "user cannot be null"); index(user, userMapper); } @Override public void addUsers(Collection users) { indexAll(users, userMapper); } @Override public void removeUser(User user) { delete(user, userMapper); } @Override @Cacheable("user-prefix-cache") public Collection searchUserByPrefix(String domain, String prefix) { return searchByPrefix(domain, prefix, DEFAULT_TOP_N_SEARCH_USER, userMapper); } private final ElasticsearchMapper groupMapper = new ElasticsearchMapper() { @Override public String id(Group group) { return group.getGroupId(); } @Override public String type() { return "group"; } @Override public String prefixSearchSortField() { return "name-not-analyzed"; } @Override public XContentBuilder toJson(Group group) throws IOException { return XContentFactory.jsonBuilder() .startObject() .field("domain", group.getDomain()) .field("groupId", group.getGroupId()) .field("name", group.getName()) .field("description", group.getDescription()) .endObject(); } }; @Override @Async public void addGroup(Group group) { index(group, groupMapper); } @Override public void removeGroup(Group group) { delete(group, groupMapper); } @Override @Cacheable("group-prefix-cache") public Collection searchGroupByPrefix(String domain, String prefix, int size) { Collection ids = searchByPrefix(domain, prefix, size, groupMapper); List groups = new ArrayList(ids.size()); for (String id : ids) { groups.add(groupDetailsRepository.getGroupDetails(id)); } return groups; } /** * Indexes an object to elasticsearch. * This method is asynchronous. * * @param object Object to index. * @param mapper Converter to JSON. */ private void index(T object, ElasticsearchMapper mapper) { Assert.notNull(object); Assert.notNull(mapper); final String type = mapper.type(); final String id = mapper.id(object); try { final XContentBuilder source = mapper.toJson(object); log.debug("Ready to index the {} id {} into Elasticsearch: {}", type, id, stringify(source)); client().prepareIndex(indexName(type), type, id).setSource(source).execute(new ActionListener() { @Override public void onResponse(IndexResponse response) { log.debug(type + " id " + id + " was " + (response.version() == 1 ? "indexed" : "updated") + " into Elasticsearch"); } @Override public void onFailure(Throwable e) { log.error("The " + type + " id " + id + " wasn't indexed : " + stringify(source), e); } }); } catch (IOException e) { log.error("The " + type + " id " + id + " wasn't indexed", e); } } /** * Indexes an collection of objects to elasticsearch. * This method is synchronous. * * @param collection Object to index. * @param adapter Converter to JSON. */ private void indexAll(Collection collection, ElasticsearchMapper adapter) { Assert.notNull(collection); Assert.notNull(adapter); if (collection.isEmpty()) return; String type = adapter.type(); BulkRequestBuilder request = client().prepareBulk(); for (T object : collection) { String id = adapter.id(object); try { XContentBuilder source = adapter.toJson(object); IndexRequestBuilder indexRequest = client().prepareIndex(indexName(type), type, id).setSource(source); request.add(indexRequest); } catch (IOException e) { log.error("The " + type + " of id " + id + " wasn't indexed", e); } } log.debug("Ready to index {} {} into Elasticsearch.", collection.size(), type); BulkResponse response = request.execute().actionGet(); if (response.hasFailures()) { int errorCount = 0; for (BulkItemResponse itemResponse : response) { if (itemResponse.failed()) { log.error("The " + type + " of id " + itemResponse.getId() + " wasn't indexed in bulk operation: " + itemResponse.getFailureMessage()); ++errorCount; } } log.error(errorCount + " " + type + " where not indexed in bulk operation."); } else { log.debug("{} {} indexed into Elasticsearch in bulk operation.", collection.size(), type); } } /** * delete a document. * This method is asynchronous. * * @param object Object to index. * @param mapper Converter to JSON. */ private void delete(T object, ElasticsearchMapper mapper) { Assert.notNull(object); Assert.notNull(mapper); final String id = mapper.id(object); final String type = mapper.type(); log.debug("Ready to delete the {} of id {} from Elasticsearch: ", type, id); client().prepareDelete(indexName(type), type, id).execute(new ActionListener() { @Override public void onResponse(DeleteResponse deleteResponse) { if (log.isDebugEnabled()) { if (deleteResponse.notFound()) { log.debug("{} of id {} was not found therefore not deleted.", type, id); } else { log.debug("{} of id {} was deleted from Elasticsearch.", type, id); } } } @Override public void onFailure(Throwable e) { log.error("The " + type + " of id " + id + " wasn't deleted from Elasticsearch.", e); } }); } private Collection searchByPrefix(String domain, String prefix, int size, ElasticsearchMapper mapper) { try { SearchRequestBuilder searchRequest = client().prepareSearch(indexName(mapper.type())) .setTypes(mapper.type()) .setQuery(matchQuery("prefix", prefix)) .setFilter(termFilter("domain", domain)) .addFields() .setFrom(0) .setSize(size) .addSort(SortBuilders.fieldSort(mapper.prefixSearchSortField()).order(SortOrder.ASC)); if (log.isTraceEnabled()) { log.trace("elasticsearch query : " + searchRequest); } SearchResponse searchResponse = searchRequest .execute() .actionGet(); SearchHits searchHits = searchResponse.hits(); if (searchHits.totalHits() == 0) return Collections.emptyList(); SearchHit[] hits = searchHits.hits(); final List ids = new ArrayList(hits.length); for (SearchHit hit : hits) { ids.add(hit.getId()); } log.debug("search " + mapper.type() + " by prefix(\"" + domain + "\", \"" + prefix + "\") = result : " + ids); return ids; } catch (IndexMissingException e) { log.warn("The index " + indexName(mapper.type()) + " was not found in the Elasticsearch cluster."); return Collections.emptyList(); } catch (ElasticSearchException e) { log.error("Error while searching user by prefix in index " + indexName(mapper.type()), e); return Collections.emptyList(); } } /** * Stringify a document source for logging purpose. * * @param source Source of the document. * @return A string representation of the document only valid for logging purpose. */ private String stringify(XContentBuilder source) { try { return source.prettyPrint().string(); } catch (IOException e) { return ""; } } /** * Used to transform an object to it's indexed representation. */ private static interface ElasticsearchMapper { /** * Provides object id; * * @param o object. * @return object id. */ String id(T o); /** * Provides index type of this mapping. * * @return The elasticsearch index type of the object. */ String type(); /** * @return The name of the field to sort by in search by prefix. */ String prefixSearchSortField(); /** * Convert object to it's indexable JSON document representation. * * @param o object. * @return Document * @throws IOException If the creation of the JSON document failed. */ XContentBuilder toJson(T o) throws IOException; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/elasticsearch/EmbeddedElasticsearchEngine.java ================================================ package fr.ippon.tatami.service.elasticsearch; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.client.Client; import org.elasticsearch.node.Node; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; import static org.elasticsearch.node.NodeBuilder.nodeBuilder; /** * Transport client configuration. */ public class EmbeddedElasticsearchEngine implements ElasticsearchEngine { private final Logger log = LoggerFactory.getLogger(EmbeddedElasticsearchEngine.class); private Node node; @PostConstruct private void init() { log.info("Initializing Elasticsearch embedded cluster..."); node = nodeBuilder() .loadConfigSettings(false) .settings(settingsBuilder().loadFromClasspath("META-INF/elasticsearch/elasticsearch-embedded.yml")) .node(); // Looking for nodes configuration if (log.isInfoEnabled()) { final NodesInfoResponse nir = client().admin().cluster().prepareNodesInfo().execute().actionGet(); log.info("Elasticsearch client is now connected to the " + nir.nodes().length + " node(s) cluster named \"" + nir.clusterName() + "\""); } } @PreDestroy private void close() { log.info("Closing Elasticsearch embedded cluster"); node.close(); } public Client client() { return node.client(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/elasticsearch/RemoteElasticsearchEngine.java ================================================ package fr.ippon.tatami.service.elasticsearch; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.client.Client; import org.elasticsearch.client.transport.TransportClient; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; /** * Transport client configuration. */ public class RemoteElasticsearchEngine implements ElasticsearchEngine { private static final Logger log = LoggerFactory.getLogger(RemoteElasticsearchEngine.class); @Inject private Environment env; private TransportClient client; @PostConstruct private void init() { log.info("Initializing Elasticsearch remote client..."); Settings settings = ImmutableSettings.settingsBuilder() .put("cluster.name", env.getRequiredProperty("elasticsearch.cluster.name")) .build(); client = new TransportClient(settings, false); // Looking for nodes configuration String nodes = env.getRequiredProperty("elasticsearch.cluster.nodes"); String[] nodesAddresses = nodes.split(","); if (nodesAddresses.length == 0) { throw new IllegalStateException("ES client must have at least one node to connect to"); } for (String nodeAddress : nodesAddresses) { client.addTransportAddress(parseAddress(nodeAddress)); } if (log.isInfoEnabled()) { NodesInfoResponse nir = client.admin().cluster().nodesInfo(new NodesInfoRequest()).actionGet(); log.info("Elasticsearch client is now connected to the " + nir.nodes().length + " node(s) cluster named \"" + nir.clusterName() + "\""); } } @PreDestroy private void close() { log.info("Closing Elasticsearch remote client"); client.close(); } public Client client() { return client; } private InetSocketTransportAddress parseAddress(String address) { String[] addressItems = address.split(":", 2); int port = Integer.parseInt(addressItems.length > 1 ? addressItems[1] : env.getRequiredProperty("elasticsearch.cluster.default.communication.port")); return new InetSocketTransportAddress(addressItems[0], port); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/exception/ArchivedGroupException.java ================================================ package fr.ippon.tatami.service.exception; /** * This exception is thrown when a user tries to post a message to an archived group. * * @author Julien Dubois */ public class ArchivedGroupException extends Exception { public ArchivedGroupException() { } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/exception/ReplyStatusException.java ================================================ package fr.ippon.tatami.service.exception; /** * This exception is thrown when a user tries to reply to a status that does not exist. * * @author Julien Dubois */ public class ReplyStatusException extends Exception { public ReplyStatusException() { } public ReplyStatusException(String s) { super(s); } public ReplyStatusException(String s, Throwable throwable) { super(s, throwable); } public ReplyStatusException(Throwable throwable) { super(throwable); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/exception/StorageSizeException.java ================================================ package fr.ippon.tatami.service.exception; /** * This exception is thrown when a user tries to exceed his storage size. * * @author Julien Dubois */ public class StorageSizeException extends Exception { public StorageSizeException(String s) { super(s); } public StorageSizeException(Throwable throwable) { super(throwable); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/util/AnalysisUtil.java ================================================ package fr.ippon.tatami.service.util; import java.util.*; /** * Common functions for analysing key trends, user & group suggestions. */ public class AnalysisUtil { private static final int RESULTS_SIZE = 20; public static void incrementKeyCounterInMap(Map map, String key) { if (map.containsKey(key)) { Integer total = map.get(key); total++; map.put(key, total); } else { map.put(key, 1); } } public static List findMostUsedKeys(Map totalTagsCount) { ValueComparator valueComparator = new ValueComparator(totalTagsCount); TreeMap orderedTags = new TreeMap(valueComparator); orderedTags.putAll(totalTagsCount); List mostUsedTags = new ArrayList(); for (int i = 0; i <= RESULTS_SIZE; i++) { Map.Entry firstEntry = orderedTags.pollFirstEntry(); if (firstEntry != null) { mostUsedTags.add(firstEntry.getKey()); } } return mostUsedTags; } public static List reduceCollectionSize(List list, int size) { if (list.size() < size) { return list; } Collections.shuffle(list); return list.subList(0, size - 1); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/util/DomainUtil.java ================================================ package fr.ippon.tatami.service.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class for managing the user domain. * * @author Julien Dubois */ public class DomainUtil { private final Logger log = LoggerFactory.getLogger(DomainUtil.class); private DomainUtil() { } public static String getDomainFromLogin(String login) { if (login == null) { return null; } return login.substring(login.indexOf("@") + 1, login.length()); } public static String getLoginFromUsernameAndDomain(String username, String domain) { return username + "@" + domain; } public static String getUsernameFromLogin(String login) { if (login == null) { return null; } return login.substring(0, login.indexOf("@")); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/util/RandomUtil.java ================================================ package fr.ippon.tatami.service.util; import org.apache.commons.lang.RandomStringUtils; /** * Utility class for generating random Strings. * * @author Julien Dubois */ public class RandomUtil { private RandomUtil() { } public static String generatePassword() { return RandomStringUtils.randomAlphanumeric(20); } public static String generateRegistrationKey() { return RandomStringUtils.randomNumeric(20); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/service/util/ValueComparator.java ================================================ package fr.ippon.tatami.service.util; import java.util.Comparator; import java.util.Map; /** * Used to sort a Map by its values. */ public class ValueComparator implements Comparator { Map base; public ValueComparator(Map base) { this.base = base; } // This comparator is not consistent with equals, as we do not want to merge keys public int compare(String a, String b) { if (base.get(a) >= base.get(b)) { return -1; } else { return 1; } } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/atmosphere/TatamiNotification.java ================================================ package fr.ippon.tatami.web.atmosphere; import fr.ippon.tatami.service.dto.StatusDTO; import java.io.Serializable; /** * Tatami notification : contains the user to be notified and the StatusDTO to display. */ public class TatamiNotification implements Serializable { private String login; private StatusDTO statusDTO; public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public StatusDTO getStatusDTO() { return statusDTO; } public void setStatusDTO(StatusDTO statusDTO) { this.statusDTO = statusDTO; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof TatamiNotification)) return false; TatamiNotification that = (TatamiNotification) o; if (!login.equals(that.login)) return false; if (!statusDTO.equals(that.statusDTO)) return false; return true; } @Override public int hashCode() { int result = login.hashCode(); result = 31 * result + statusDTO.hashCode(); return result; } @Override public String toString() { return "TatamiNotification{" + "login='" + login + '\'' + ", statusDTO=" + statusDTO + "} " + super.toString(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/ActionStatus.java ================================================ package fr.ippon.tatami.web.rest.dto; import java.io.Serializable; /** * Reply to a Status. */ public class ActionStatus implements Serializable { private Boolean favorite = null; private Boolean shared = null; private Boolean announced = null; public Boolean isFavorite() { return favorite; } public void setFavorite(Boolean favorite) { this.favorite = favorite; } public Boolean isShared() { return shared; } public void setShared(Boolean shared) { this.shared = shared; } public Boolean isAnnounced() { return announced; } public void setAnnounced(Boolean announced) { this.announced = announced; } @Override public String toString() { return "ActionStatus{" + "favorite=" + favorite + ", shared=" + shared + ", announced=" + announced + "} " + super.toString(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/EmailAndUsername.java ================================================ package fr.ippon.tatami.web.rest.dto; import java.io.Serializable; /** * DTO containing the user name and e-mail. */ public class EmailAndUsername implements Serializable { private String email; private String username; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/Preferences.java ================================================ package fr.ippon.tatami.web.rest.dto; import fr.ippon.tatami.domain.User; import org.apache.commons.lang.StringUtils; import java.io.Serializable; /** * Stores a user's preferences. */ public class Preferences implements Serializable { private Boolean mentionEmail = false; private Boolean weeklyDigest = false; private Boolean dailyDigest = false; private Boolean rssUidActive = false; private String rssUid; public Preferences() { } public Preferences(User user) { this.mentionEmail = user.getPreferencesMentionEmail(); this.weeklyDigest = user.getWeeklyDigestSubscription(); this.dailyDigest = user.getDailyDigestSubscription(); if (!StringUtils.isEmpty(user.getRssUid())) { this.rssUidActive = true; this.rssUid = user.getRssUid(); } } public Boolean getMentionEmail() { return mentionEmail; } public void setMentionEmail(Boolean mentionEmail) { this.mentionEmail = mentionEmail; } public Boolean getRssUidActive() { return this.rssUidActive; } public void setRssUidActive(boolean rssUidActive) { this.rssUidActive = rssUidActive; } public String getRssUid() { return rssUid; } public void setRssUid(String rssUid) { this.rssUid = rssUid; } public Boolean getWeeklyDigest() { return weeklyDigest; } public void setWeeklyDigest(Boolean weeklyDigest) { this.weeklyDigest = weeklyDigest; } public Boolean getDailyDigest() { return dailyDigest; } public void setDailyDigest(Boolean dailyDigest) { this.dailyDigest = dailyDigest; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/Reply.java ================================================ package fr.ippon.tatami.web.rest.dto; import java.io.Serializable; /** * Reply to a Status. */ public class Reply implements Serializable { private String statusId; private String content; public String getStatusId() { return statusId; } public void setStatusId(String statusId) { this.statusId = statusId; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return "Reply{" + "statusId='" + statusId + '\'' + ", content='" + content + '\'' + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/SearchResults.java ================================================ package fr.ippon.tatami.web.rest.dto; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.service.dto.UserDTO; import java.io.Serializable; import java.util.Collection; /** * Search result for the global search engine. */ public class SearchResults implements Serializable { private Collection tags; private Collection users; private Collection groups; public Collection getTags() { return tags; } public void setTags(Collection tags) { this.tags = tags; } public Collection getUsers() { return users; } public void setUsers(Collection users) { this.users = users; } public Collection getGroups() { return groups; } public void setGroups(Collection groups) { this.groups = groups; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/Tag.java ================================================ package fr.ippon.tatami.web.rest.dto; import java.io.Serializable; /** * A Tag. */ public class Tag implements Serializable { private String name; private boolean followed; public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isFollowed() { return followed; } public void setFollowed(boolean followed) { this.followed = followed; } private boolean trendingUp = false; public boolean isTrendingUp() { return trendingUp; } public void setTrendingUp(boolean trendingUp) { this.trendingUp = trendingUp; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Tag tag = (Tag) o; return name.equals(tag.name); } @Override public int hashCode() { return name.hashCode(); } @Override public String toString() { return "Tag{" + "name='" + name + '\'' + ", followed='" + followed + '\'' + ", trendingUp='" + trendingUp + '\'' + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/Trend.java ================================================ package fr.ippon.tatami.web.rest.dto; import java.io.Serializable; /** * A trend : a tag that is trending up or down. */ public class Trend implements Serializable { private String tag; private boolean trendingUp; public boolean isTrendingUp() { return trendingUp; } public void setTrendingUp(boolean trendingUp) { this.trendingUp = trendingUp; } public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Trend trend = (Trend) o; return tag.equals(trend.tag); } @Override public int hashCode() { return tag.hashCode(); } @Override public String toString() { return "Trend{" + "tag='" + tag + '\'' + ", trendingUp=" + trendingUp + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/UserActionStatus.java ================================================ package fr.ippon.tatami.web.rest.dto; import java.io.Serializable; /** * This class is used in order to specify the action of PATCH request (cf vUserList.js). * The action can be "addFriend" ( friendShip = true ) or "activate/desactivate" ( activate = true ). */ public class UserActionStatus implements Serializable { private Boolean activate = null; private Boolean friendShip = null; private Boolean isFriend = null; public Boolean getFriend() { return isFriend; } public void setFriend(Boolean friend) { isFriend = friend; } public Boolean getFriendShip() { return friendShip; } public void setFriendShip(Boolean friendShip) { this.friendShip = friendShip; } public Boolean getActivate() { return activate; } public void setActivate(Boolean activate) { this.activate = activate; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/rest/dto/UserPassword.java ================================================ package fr.ippon.tatami.web.rest.dto; import java.io.Serializable; /** * Form bean used to change the user's password. */ public class UserPassword implements Serializable { private String oldPassword; private String newPassword; private String newPasswordConfirmation; public String getOldPassword() { return oldPassword; } public void setOldPassword(String oldPassword) { this.oldPassword = oldPassword; } public String getNewPassword() { return newPassword; } public void setNewPassword(String newPassword) { this.newPassword = newPassword; } public String getNewPasswordConfirmation() { return newPasswordConfirmation; } public void setNewPasswordConfirmation(String newPasswordConfirmation) { this.newPasswordConfirmation = newPasswordConfirmation; } @Override public String toString() { return "UserPassword{" + "oldPassword='" + oldPassword + '\'' + ", newPassword='" + newPassword + '\'' + ", newPasswordConfirmation='" + newPasswordConfirmation + '\'' + '}'; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/syndic/SyndicTimelineController.java ================================================ /* * To change this template, choose Tools | Templates * and open the template in the editor. */ package fr.ippon.tatami.web.syndic; import fr.ippon.tatami.service.TimelineService; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.dto.StatusDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import javax.inject.Inject; import java.util.Collection; import java.util.Locale; /** * @author Pierre Rust */ @Controller public class SyndicTimelineController { private final Logger log = LoggerFactory.getLogger(SyndicTimelineController.class); @Inject private TimelineService timelineService; @Inject private UserService userService; @Inject private MessageSource messageSource; /** * GET /syndic/{rssUid} -> get the latest statuses from user username * corresponding to the uid */ @RequestMapping(value = "/syndic/{rssUid}", method = RequestMethod.GET, produces = "application/rss+xml") @ResponseBody public ModelAndView listStatusForUser(@PathVariable String rssUid) { String login = userService.getLoginByRssUid(rssUid); if (login == null) { throw new UnknownRssChannelException("Could not find requested rss channel"); } int count = 20; //Default value log.debug("RSS request to get someone's status (login={}).", login); Collection statuses = timelineService.getUserTimeline(login, count, null, null); ModelAndView mav = new ModelAndView("syndicView"); // i18n Locale locale = LocaleContextHolder.getLocale(); Object[] params = {login}; String feedTitle = messageSource.getMessage("tatami.rss.timeline.title", params, locale); String feedDesc = messageSource.getMessage("tatami.rss.timeline.description", params, locale); mav.addObject("feedTitle", feedTitle); mav.addObject("feedDescription", feedDesc); mav.addObject("statusBaseLink", "/tatami/home/"); // the link must point the actual content and not to the rss channel mav.addObject("feedLink", "/tatami/"); mav.addObject("feedContent", statuses); return mav; } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/syndic/SyndicView.java ================================================ package fr.ippon.tatami.web.syndic; import com.sun.syndication.feed.rss.Channel; import com.sun.syndication.feed.rss.Content; import com.sun.syndication.feed.rss.Item; import fr.ippon.tatami.service.dto.StatusDTO; import org.pegdown.PegDownProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.view.feed.AbstractRssFeedView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * View used to generate the RSS stream corresponding to the timeline * * @author Pierre Rust */ public class SyndicView extends AbstractRssFeedView { private final Logger log = LoggerFactory.getLogger(SyndicView.class); @Override protected void buildFeedMetadata(Map model, Channel feed, HttpServletRequest request) { // this is mandatory: a feed with no title is invalid and is not rendered // by the actual view implementation String title = (String) model.get("feedTitle"); if (title == null) { title = "This RSS feed does not exist."; } String description = (String) model.get("feedDescription"); if (description == null) { description = "Either you typed a wrong URL, or the feed was removed by the user to which it belongs."; } String link = (String) model.get("feedLink"); if (link == null) { link = ""; } feed.setTitle(title); feed.setDescription(description); feed.setLink(link); feed.setEncoding("UTF-8"); super.buildFeedMetadata(model, feed, request); } @Override @SuppressWarnings("unchecked") protected List buildFeedItems(Map model, HttpServletRequest hsr, HttpServletResponse hsr1) throws Exception { List items = new ArrayList(); Collection listContent = (Collection) model.get("feedContent"); if (listContent == null) { return items; } String statusBaseLink = (String) model.get("statusBaseLink"); for (StatusDTO tempContent : listContent) { Item item = new Item(); String statusText = tempContent.getContent(); PegDownProcessor processor = new PegDownProcessor(); String htmlText = processor.markdownToHtml(statusText); log.debug("feed html content {}", htmlText); // url handling for mention & tags htmlText = convertLinks(htmlText); Content content = new Content(); content.setType(Content.HTML); content.setValue(htmlText); item.setContent(content); // build link for the status StringBuilder linkBuilder = new StringBuilder(statusBaseLink); linkBuilder .append("status/") .append(tempContent.getStatusId()); item.setTitle(statusText.substring(0, Math.min(30, statusText.length()))); item.setLink(linkBuilder.toString()); item.setPubDate(tempContent.getStatusDate()); items.add(item); } return items; } /** * convert #tag and @mention to html links * * @param htmlText the original text * @return html with converted links */ private String convertLinks(String htmlText) { // inside status : users (mention) & tags // the regexp are converted from tatami customized marked.js Pattern p = Pattern.compile("@([A-Za-z0-9!#$%'*+\\/=?\\^_`{|}~\\-]+(?:\\.[A-Za-z0-9!#$%'*+\\/=?\\^_`{|}~\\-]+)*)"); Matcher m = p.matcher(htmlText); StringBuffer mentionSb = new StringBuffer(); while (m.find()) { m.appendReplacement(mentionSb, "$0"); } m.appendTail(mentionSb); p = Pattern.compile("#([^\\s!\"&#$%'()*+,./:;<=>?@\\\\\\[\\]^_`{|}~-]+(?:\\.[^\\s !\"&#$%'()*+,./:;<=>?@\\\\\\[\\]^_`{|}~-]+)*)"); m = p.matcher(mentionSb.toString()); StringBuffer tagSb = new StringBuffer(); while (m.find()) { m.appendReplacement(tagSb, "$0"); } m.appendTail(tagSb); return tagSb.toString(); } } ================================================ FILE: services/src/main/java/fr/ippon/tatami/web/syndic/UnknownRssChannelException.java ================================================ /* * To change this template, choose Tools | Templates * and open the template in the editor. */ package fr.ippon.tatami.web.syndic; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; /** * @author Pierre Rust */ @ResponseStatus(value = HttpStatus.NOT_FOUND) public class UnknownRssChannelException extends RuntimeException { public UnknownRssChannelException(String msg) { super(msg); } } ================================================ FILE: services/src/main/java/me/prettyprint/hom/CassandraPersistenceProvider.java ================================================ package me.prettyprint.hom; import org.apache.openjpa.persistence.EntityManagerFactoryImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.persistence.EntityManagerFactory; import javax.persistence.spi.PersistenceProvider; import javax.persistence.spi.PersistenceUnitInfo; import java.util.Map; /** * This is a temporary hack : this class is needed at startup by the HOM configuration, * but is not provided (and anyway HOM doesn't use it...). */ public class CassandraPersistenceProvider implements PersistenceProvider { private static final Logger log = LoggerFactory.getLogger(CassandraPersistenceProvider.class); private Map defProperties; public CassandraPersistenceProvider() { } public CassandraPersistenceProvider(Map map) { this.defProperties = map; } @Override public EntityManagerFactory createContainerEntityManagerFactory( PersistenceUnitInfo info, Map map) { log.debug("creating EntityManagerFactory {} with properties {} ", null, map); return null; } @Override public EntityManagerFactory createEntityManagerFactory(String emName, Map map) { log.debug("creating EntityManagerFactory {} with properties {} ", emName, map); if (map == null || map.isEmpty()) { return new EntityManagerFactoryImpl(); } return new EntityManagerFactoryImpl(); } } ================================================ FILE: services/src/main/resources/META-INF/elasticsearch/elasticsearch-embedded.yml ================================================ ##################### ElasticSearch Configuration For Embedded Cluster ##################### # This file contains the configuration of the embedded elasticsearch cluster, cluster.name: tatamiCluster node: local: true name: "tatami-embedded-node" path: data: ${tatami.elasticsearch.path.data}/data work: ${tatami.elasticsearch.path.data}/work logs: ${tatami.elasticsearch.path.log} http.enabled: ${tatami.elasticsearch.http.enabled} ================================================ FILE: services/src/main/resources/META-INF/elasticsearch/index/group.json ================================================ { "settings": { "number_of_shards": 1, "number_of_replicas": 0, "analysis": { "analyzer": { "default": { "type": "custom", "tokenizer": "standard", "filter": ["lowercase", "stop_francais", "fr_stemmer", "asciifolding", "elision"] }, "lowercase": { "type": "custom", "tokenizer": "keyword", "filter": ["lowercase" ] }, "prefixIndex" : { "type" : "custom", "tokenizer" : "standard", "filter" : [ "standard", "lowercase", "asciifolding", "elision", "edgeNGram" ] }, "prefixSearch" : { "type" : "custom", "tokenizer" : "standard", "filter" : [ "standard", "lowercase", "asciifolding", "elision" ] } }, "filter": { "stop_francais": { "type": "stop", "stopwords": ["_french_"] }, "fr_stemmer": { "type": "stemmer", "name": "french" }, "elision": { "type": "elision", "articles": ["l", "m", "t", "qu", "n", "s", "j", "d"] }, "edgeNGram" : { "type" : "edgeNGram", "min_gram" : 1, "max_gram" : 30 } } } }, "mappings": { "group": { "properties": { "groupId": { "type": "string", "index": "not_analyzed", "include_in_all": false }, "domain": { "type": "string", "analyzer": "lowercase" }, "name": { "type": "multi_field", "path": "just_name", "fields": { "name": { "type": "string" }, "name-not-analyzed": { "type": "string", "analyzer": "lowercase" }, "prefix" : { "type" : "string", "index_analyzer" : "prefixIndex", "search_analyzer" : "prefixSearch" } } }, "description": { "type": "string" } } } } } ================================================ FILE: services/src/main/resources/META-INF/elasticsearch/index/status.json ================================================ { "settings": { "number_of_shards": 1, "number_of_replicas": 0, "analysis": { "analyzer": { "default": { "type": "custom", "tokenizer": "standard", "filter": ["lowercase", "stop_francais", "fr_stemmer", "asciifolding", "elision"] }, "lowercase": { "type": "custom", "tokenizer": "keyword", "filter": ["lowercase" ] } }, "filter": { "stop_francais": { "type": "stop", "stopwords": ["_french_"] }, "fr_stemmer": { "type": "stemmer", "name": "french" }, "elision": { "type": "elision", "articles": ["l", "m", "t", "qu", "n", "s", "j", "d"] } } } }, "mappings": { "status": { "properties": { "statusId": { "type": "string", "index": "not_analyzed", "include_in_all": false }, "domain": { "type": "string", "analyzer": "lowercase", "include_in_all": false }, "groupId": { "type": "string", "index": "not_analyzed", "include_in_all": false }, "publicGroup": { "type": "boolean", "include_in_all": false }, "username": { "type": "string", "analyzer": "lowercase" }, "content": { "type": "string" }, "statusDate": { "type": "date", "include_in_all": false } } } } } ================================================ FILE: services/src/main/resources/META-INF/elasticsearch/index/user.json ================================================ { "settings": { "number_of_shards": 1, "number_of_replicas" : 0, "analysis": { "analyzer": { "default": { "type": "custom", "tokenizer": "standard", "filter": [ "lowercase", "stop_francais", "fr_stemmer", "asciifolding", "elision" ] }, "lowercase": { "type": "custom", "tokenizer": "keyword", "filter": [ "lowercase" ] }, "prefixIndex" : { "type" : "custom", "tokenizer" : "standard", "filter" : [ "standard", "lowercase", "asciifolding", "elision", "edgeNGram" ] }, "prefixSearch" : { "type" : "custom", "tokenizer" : "standard", "filter" : [ "standard", "lowercase", "asciifolding", "elision" ] } }, "filter": { "stop_francais": { "type": "stop", "stopwords": [ "_french_" ] }, "fr_stemmer": { "type": "stemmer", "name": "french" }, "elision": { "type": "elision", "articles": [ "l", "m", "t", "qu", "n", "s", "j", "d" ] }, "edgeNGram" : { "type" : "edgeNGram", "min_gram" : 1, "max_gram" : 30 } } } }, "mappings": { "user": { "properties": { "login": { "type": "string", "analyzer": "lowercase", "include_in_all": false }, "domain": { "type": "string", "analyzer": "lowercase", "include_in_all": false }, "username": { "type" : "multi_field", "path" : "just_name", "fields" : { "username": { "type" : "string", "analyzer" : "lowercase" }, "prefix" : { "type" : "string", "index_analyzer" : "prefixIndex", "search_analyzer" : "prefixSearch" } } }, "firstName": { "type" : "multi_field", "path" : "just_name", "fields" : { "firstName": { "type": "string", "analyzer" : "default" }, "prefix" : { "type" : "string", "index_analyzer" : "prefixIndex", "search_analyzer" : "prefixSearch" } } }, "lastName": { "type" : "multi_field", "path" : "just_name", "fields" : { "lastName": { "type": "string", "analyzer" : "default" }, "prefix" : { "type" : "string", "index_analyzer" : "prefixIndex", "search_analyzer" : "prefixSearch" } } } } } } } ================================================ FILE: services/src/main/resources/META-INF/elasticsearch/logging.yml ================================================ rootLogger: WARN, file logger: # log action execution errors for easier debugging action: DEBUG # reduce the logging for aws, too much is logged under the default INFO com.amazonaws: WARN # gateway #gateway: DEBUG #index.gateway: DEBUG # peer shard recovery #indices.recovery: DEBUG # discovery #discovery: TRACE index.search.slowlog: TRACE, index_search_slow_log_file additivity: index.search.slowlog: false appender: console: type: console layout: type: consolePattern conversionPattern: "[%d{ISO8601}][%-5p][%-25c] %m%n" file: type: dailyRollingFile file: ${path.logs}/${cluster.name}.log datePattern: "'.'yyyy-MM-dd" layout: type: pattern conversionPattern: "[%d{ISO8601}][%-5p][%-25c] %m%n" index_search_slow_log_file: type: dailyRollingFile file: ${path.logs}/${cluster.name}_index_search_slowlog.log datePattern: "'.'yyyy-MM-dd" layout: type: pattern conversionPattern: "[%d{ISO8601}][%-5p][%-25c] %m%n" ================================================ FILE: services/src/main/resources/META-INF/spring/applicationContext-metrics.xml ================================================ ================================================ FILE: services/src/main/resources/META-INF/spring/applicationContext-security.xml ================================================ ================================================ FILE: services/src/main/resources/META-INF/tatami/customization.properties ================================================ # list of authorized themes : # useful if you have only a few official company themes and must restrict your users to these #tatami.authorized.theme=bootstrap,cerulean,cosmo,journal,readable,simplex,spacelab,spruce,superhero,united ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/common ================================================ #** * msg * * Shorthand macro to retrieve locale sensitive message from messages_xxx.properties *# #macro( msg $key )$messages.getMessage($key, null, $locale)#end ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/dailyDigestEmail ================================================ #parse("common") #msg("dailyDigest.greeting") ${user.Login}, #if ($statuses.size() == 1) #msg("dailyDigest.textOneMessages1") #msg("dailyDigest.from") @$statuses[0].Username $statuses[0].Content #elseif ($statuses.size() > 1) #if (${nbStatus} >10 ) #msg("dailyDigest.textSeveralMessagesSelection1") ${nbStatus} #msg("dailyDigest.textSeveralMessagesSelection2") #else #msg("dailyDigest.textSeveralMessages1") ${nbStatus} #msg("dailyDigest.textSeveralMessages2") #end #foreach( $status in $statuses ) * #msg("dailyDigest.from") @$status.Username $status.Content #end #msg("dailyDigest.textSeveralMessages3") #else #msg("dailyDigest.textNoMessage") #end #if ($suggestedUsers.size()== 1) #msg("dailyDigest.oneSuggestedUsers1") #elseif ($suggestedUsers.size() > 1) #msg("dailyDigest.suggestedUsers1") #foreach( $suggestedUser in $suggestedUsers ) * $suggestedUser.Username #end #else #msg("dailyDigest.noSuggestedUsers") #end #msg("dailyDigest.urlText") $tatamiUrl #msg("dailyDigest.endText1") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/deactivatedUserEmail ================================================ #parse("common") #msg("deactivatedUser.greeting") ${username}, #msg("deactivatedUser.text1") #msg("deactivatedUser.text2") #msg("deactivatedUser.text3") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/invitationMessageEmail ================================================ #parse("common") #msg("invitationMessage.greeting"), ${user} #msg("invitationMessage.text1") #msg("invitationMessage.text2") ${invitationUrl} #msg("invitationMessage.text3") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/lostPasswordEmail ================================================ #parse("common") #msg("lostPassword.greeting") ${user.Login}, #msg("lostPassword.text1") #msg("lostPassword.text2") ${reinitUrl} #msg("lostPassword.text3") #msg("lostPassword.text4") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/messages/messages_en.properties ================================================ email.signature=Ippon Technologies registration.title=Tatami Registration registration.greeting=Dear registration.text1=Your Tatami account has been created, please click on the URL below to activate it: registration.text2=Regards, lostPassword.title=Tatami Forgotten Password lostPassword.greeting=Dear lostPassword.text1=Someone asked to reset your password. lostPassword.text2=If you want to reset your password, please click on the link below: lostPassword.text3=If you do not want to reset your password, you can safely ignore this message. lostPassword.text4=Regards, validation.title=Tatami Account Confirmed validation.greeting=Dear validation.text1=Your Tatami account has been confirmed, here is your password: validation.text2=Regards, passwordReinitialized.title=Tatami Password Reset passwordReinitialized.greeting=Dear passwordReinitialized.text1=Your Tatami password has been reset, here is your new password: passwordReinitialized.text2=Regards, userPrivateMessage.title=You received a private message on Tatami userPrivateMessage.greeting=Dear userPrivateMessage.text1=You received a private message on Tatami from userPrivateMessage.text2=You can see this status on Tatami: userPrivateMessage.text3=Regards, userMention.title=Someone mentioned you on Tatami userMention.greeting=Dear userMention.text1=Someone mentioned you on Tatami: userMention.text2=You can see this status on Tatami: userMention.text3=Regards, reportedStatus.title=Reported Status on Tatami reportedStatus.greeting=Dear admin, reportedStatus.text1=Someone reported a status on Tatami: reportedStatus.text2=You can see this status on Tatami: reportedStatus.text3=Regards, deactivatedUser.title=Your Account is Deactivated deactivatedUser.greeting=Dear deactivatedUser.text1=An admin has deactivated your account. deactivatedUser.text2=Please contact your administrator for more details. deactivatedUser.text3=Regards, invitationMessage.title=One of your co-workers invited you to Tatami invitationMessage.greeting=Hello, invitationMessage.text1=invited you to Tatami, your company's social network. invitationMessage.text2=You can register on: invitationMessage.text3=Regards, dailyDigest.title=Tatami Daily Digest dailyDigest.greeting=Dear dailyDigest.from=from dailyDigest.textOneMessages1=You have one message in your timeline today: dailyDigest.textSeveralMessages1=You have dailyDigest.textSeveralMessages2=messages in your timeline today. dailyDigest.textSeveralMessages3=Login to Tatami to read your other messages. dailyDigest.textSeveralMessagesSelection1=You have dailyDigest.textSeveralMessagesSelection2=message in your timeline today. Here are some of them: dailyDigest.textNoMessage=There are no messages in your timeline today. dailyDigest.oneSuggestedUsers1=Suggested users you might want to follow: dailyDigest.suggestedUsers1=To get more interesting updates, you may want to follow these users: dailyDigest.noSuggestedUsers=You can find more interesting people on Tatami. dailyDigest.endText1=See you tomorrow for more updates dailyDigest.urlText=Don't miss anything, go to Tatami! weeklyDigest.title=Tatami Weekly Digest weeklyDigest.greeting=Dear weeklyDigest.from=from weeklyDigest.textOneMessages1=You have one message in your timeline: weeklyDigest.textSeveralMessages1=You have weeklyDigest.textSeveralMessages2=messages in your timeline. weeklyDigest.textSeveralMessages3=Login to Tatami to read your other messages. weeklyDigest.textSeveralMessagesSelection1=You have weeklyDigest.textSeveralMessagesSelection2=message in your timeline today. Here are some of them: weeklyDigest.textNoMessage=There are no messages in your timeline. weeklyDigest.oneSuggestedUsers1=Suggested users you might want to follow: weeklyDigest.suggestedUsers1=To get more interesting updates, you may want to follow these users: weeklyDigest.noSuggestedUsers=You can find more interesting people on Tatami. weeklyDigest.oneSuggestedGroups1=To get more interesting updates, you may want to join this group: weeklyDigest.suggestedGroups1=To get more interesting updates, you may want to join to these groups: weeklyDigest.noSuggestedGroups=You can find more interesting groups on Tatami. weeklyDigest.endText1=See you next week for more updates weeklyDigest.urlText=Don't miss anything, go to Tatami! ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/messages/messages_fr.properties ================================================ email.signature=Ippon Technologies. registration.title=Activation de votre compte Tatami registration.greeting=Cher registration.text1=Votre compte Tatami a été créé, pour l'activer merci de cliquer sur le lien ci-dessous : registration.text2=Cordialement, lostPassword.title=Perte de votre mot de passe Tatami lostPassword.greeting=Cher lostPassword.text1=Une ré-initialisation de votre mot de passe a été demandée lostPassword.text2=Si vous souhaitez ré-initialiser votre mot de passe, cliquez sur le lien ci-dessous : lostPassword.text3=Si vous ne souhaitez pas ré-initialiser votre mot de passe, vous pouvez ignorer ce message. lostPassword.text4=Cordialement, validation.title=validation de votre compte Tatami validation.greeting=Cher validation.text1=Votre compte Tatami a été validé, voici votre mot de passe : validation.text2=Cordialement, passwordReinitialized.title=Ré-initialisation de votre mot de passe Tatami passwordReinitialized.greeting=Cher passwordReinitialized.text1=Votre mot de passe Tatami a été ré-initialisé, voici votre nouveau mot de passe : passwordReinitialized.text2=Cordialement, userPrivateMessage.title=Vous avez reçu un message privé sur Tatami userPrivateMessage.greeting=Cher userPrivateMessage.text1=Vous avez reçu un message privé sur Tatami de la part de userPrivateMessage.text2=Pour voir ce message sur Tatami : userPrivateMessage.text3=Cordialement, userMention.title=Vous avez été mentionné sur Tatami userMention.greeting=Cher userMention.text1=Vous avez été mentionné sur Tatami : userMention.text2=Pour voir ce message sur Tatami : userMention.text3=Cordialement, reportedStatus.title=Statut Signalé sur Tatami reportedStatus.greeting=Cher Admin reportedStatus.text1=Quelqu'un a signalé un statut sur Tatami reportedStatus.text2=Pour voir ce message sur Tatami: reportedStatus.text3=Cordialement, deactivatedUser.title=Your Account is Deactivated deactivatedUser.greeting=Cher deactivatedUser.text1=An admin has deactivated your account. deactivatedUser.text2=Please contact your administrator for more details. deactivatedUser.text3=Cordialement, invitationMessage.title=Un de vos collègues vous a invité sur Tatami invitationMessage.greeting=Bonjour invitationMessage.text1=vous a invité sur Tatami, votre réseau social d'entreprise. invitationMessage.text2=Pour vous enregistrer, cliquez ici : invitationMessage.text3=Cordialement, dailyDigest.title=Tatami - Résumé quotidien de votre actualité dailyDigest.greeting=Bonjour dailyDigest.from=de dailyDigest.textOneMessages1=Vous avez un message aujourd'hui \: dailyDigest.textSeveralMessages1=Vous avez dailyDigest.textSeveralMessages2=messages aujourd'hui. dailyDigest.textSeveralMessages3=Pour voir vos autres messages, connectez vous sur tatami ! dailyDigest.textSeveralMessagesSelection1=Vous avez dailyDigest.textSeveralMessagesSelection2=messages aujourd'hui. En voici une sélection : dailyDigest.textNoMessage=Vous n'avez pas de message aujourd'hui. dailyDigest.oneSuggestedUsers1=Pour avoir un visions plus riche de l'actualité, nous vous conseillons de vous abonner à l'utilisateur suivant : dailyDigest.suggestedUsers1=Pour avoir un visions plus riche de l'actualité, nous vous conseillons de vous abonner aux utilisateurs suivants : dailyDigest.noSuggestedUsers=Vous pouvez trouver plus de personnes à suivre sur tatami. dailyDigest.endText1=merci et à demain, dailyDigest.urlText=Ne manquez rien de l'actualité, connectez vous sur Tatami ! weeklyDigest.title=Tatami - Résumé hebdomadaire de votre actualité weeklyDigest.greeting=Bonjour weeklyDigest.from=de weeklyDigest.textOneMessages1=Vous avez reçu un message cette semaine \: weeklyDigest.textSeveralMessages1=Vous avez reçu weeklyDigest.textSeveralMessages2=messages cette semaine. weeklyDigest.textSeveralMessages3=Pour voir vos autres messages, connectez vous sur tatami ! weeklyDigest.textSeveralMessagesSelection1=Vous avez reçu weeklyDigest.textSeveralMessagesSelection2=messages cette semaine. En voici une sélection : weeklyDigest.textNoMessage=Vous n'avez pas reçu de message cette semaine. weeklyDigest.oneSuggestedUsers1=Pour avoir un visions plus riche de l'actualité, nous vous conseillons de vous abonner à l'utilisateur suivant : weeklyDigest.suggestedUsers1=Pour avoir un visions plus riche de l'actualité, nous vous conseillons de vous abonner aux utilisateurs suivants : weeklyDigest.noSuggestedUsers=Vous pouvez trouver plus de personnes à suivre sur tatami. weeklyDigest.oneSuggestedGroups1=Pour avoir un visions plus riche de l'actualité, nous vous conseillons de vous inscrire au groupe suivant : weeklyDigest.suggestedGroups1=Pour avoir un visions plus riche de l'actualité, nous vous conseillons de vous inscrire aux groupes suivants : weeklyDigest.noSuggestedGroups=Vous pouvez trouver plus de groupes à suivre sur tatami. weeklyDigest.endText1=merci et à la semaine prochaine, weeklyDigest.urlText=Ne manquez rien de l'actualité, connectez vous sur Tatami ! ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/passwordReinitializedEmail ================================================ #parse("common") #msg("passwordReinitialized.greeting") ${user.Login}, #msg("passwordReinitialized.text1") ${password} #msg("passwordReinitialized.text2") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/registrationEmail ================================================ #parse("./common") #msg("registration.greeting") ${user.Login}, #msg("registration.text1") ${registrationUrl} #msg("registration.text2") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/reportedStatusEmail ================================================ #parse("common") #msg("reportedStatus.greeting"), #msg("reportedStatus.text1") ${status.Content} #msg("reportedStatus.text2") ${statusUrl} #msg("reportedStatus.text3") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/userMentionEmail ================================================ #parse("common") #msg("userMention.greeting") ${user.Username}, #msg("userMention.text1") @${status.Username} ${status.Content} #msg("userMention.text2") ${statusUrl} #msg("userMention.text3") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/userPrivateMessageEmail ================================================ #parse("common") #msg("userPrivateMessage.greeting") ${user.Username}, #msg("userPrivateMessage.text1") @${status.Username} : ${status.Content} #msg("userPrivateMessage.text2") ${statusUrl} #msg("userPrivateMessage.text3") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/validationEmail ================================================ #parse("common") #msg("validation.greeting") ${user.Login}, #msg("validation.text1") ${password} #msg("validation.text2") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/mails/weeklyDigestEmail ================================================ #parse("common") #msg("weeklyDigest.greeting") ${user.Login}, #if ($statuses.size() == 1) #msg("weeklyDigest.textOneMessages1") #msg("weeklyDigest.from") @$statuses[0].Username $statuses[0].Content #elseif ($statuses.size() > 1) #if (${nbStatus} >10 ) #msg("weeklyDigest.textSeveralMessagesSelection1") ${nbStatus} #msg("weeklyDigest.textSeveralMessagesSelection2") #else #msg("weeklyDigest.textSeveralMessages1") ${nbStatus} #msg("weeklyDigest.textSeveralMessages2") #end #foreach( $status in $statuses ) * #msg("weeklyDigest.from") @$status.Username $status.Content #end #msg("weeklyDigest.textSeveralMessages3") #else #msg("weeklyDigest.textNoMessage") #end #if ($suggestedUsers.size()== 1) #msg("weeklyDigest.oneSuggestedUsers1") #elseif ($suggestedUsers.size() > 1) #msg("weeklyDigest.suggestedUsers1") #foreach( $suggestedUser in $suggestedUsers ) * $suggestedUser.Username #end #else #msg("weeklyDigest.noSuggestedUsers") #end #if ($suggestedGroups.size()== 1) #msg("weeklyDigest.oneSuggestedGroups1") #elseif ($suggestedGroups.size() > 1) #msg("weeklyDigest.suggestedGroups1") #foreach( $suggestedGroup in $suggestedGroups ) * $suggestedGroup.Name $suggestedGroup.Description #end #else #msg("weeklyDigest.noSuggestedGroups") #end #msg("weeklyDigest.urlText") $tatamiUrl #msg("weeklyDigest.endText1") #msg("email.signature") ================================================ FILE: services/src/main/resources/META-INF/tatami/tatami.properties ================================================ # tatami.version is used to version static resources and thus enable a long cache value. tatami.version=${tatami.version} tatami.url=${tatami.url} #Web app configuration tatami.wro4j.enabled=${tatami.wro4j.enabled} tatami.google.analytics.key=${tatami.google.analytics.key} tatami.message.reloading.enabled=${tatami.message.reloading.enabled} # Spring Security channel security, see http://static.springsource.org/spring-security/site/docs/3.1.x/reference/ns-config.html#ns-requires-channel tatami.connection.security=${tatami.connection.security} # Automatically register users. WARNING : should only be set to true for testing the application tatami.automatic.registration=${tatami.automatic.registration} #Monitoring using Yammer Metrics tatami.metrics.graphite.host=${tatami.metrics.graphite.host} tatami.metrics.graphite.port=${tatami.metrics.graphite.port} #User configuration tatami.admin.users=jdubois@ippon.fr,julien.dubois@gmail.com,vdebelil@ippon.fr,ggruel@ippon.fr,snomis@ippon.fr,rlheritier@ippon.fr,jrisch@ippon.fr,bscott@ippon.fr,khegeland@ippon.fr tatami.ldapauth.domain=ippon.fr tatami.ldapauth.url=${tatami.ldapauth.url} tatami.ldapauth.searchbase=${tatami.ldapauth.searchbase} tatami.ldapauth.searchfilter=${tatami.ldapauth.searchfilter} #Attachment thumbnail generation #Files extension for which we create thumbnails, comma separated tatami.attachment.thumbnail.extensions=.gif,.jpg,.jpeg,.png tatami.google.clientId=${tatami.google.clientId} tatami.google.clientSecret=${tatami.google.clientSecret} #E-mail configuration smtp.host=${tatami.smtp.host} smtp.port=${tatami.smtp.port} smtp.user=${tatami.smtp.user} smtp.password=${tatami.smtp.password} smtp.from=${tatami.smtp.from} smtp.tls=${tatami.smtp.tls} #Apple push notifications apple.push.certificate=${apple.push.certificate} apple.push.password=${apple.push.password} #Cassandra configuration cassandra.host=127.0.0.1:9160 cassandra.clusterName=Tatami cluster cassandra.keyspace=tatami # Search engine configuration : you can use either Elastic Search in embedded or in remote mode # - In embedded mode, Elastic Search runs inside Tatami : this is useful for development, test, and small installations # - In remote mode, Elastic Search runs as a separate node (or even cluster), which is useful for large installations # # You can change this configuration mode later : a full reindex of the search engine can be triggered on the # administration page elasticsearch.engine.mode=${tatami.elasticsearch.engine.mode} elasticsearch.indexNamePrefix=tatami # Must be the same as the ES cluster.name variable (see elasticsearch.yml) elasticsearch.cluster.name=tatamiCluster #Cluster nodes example: elasticsearch.cluster.nodes=10.160.0.12,10.160.0.13:9300 elasticsearch.cluster.nodes=127.0.0.1:9300 elasticsearch.cluster.default.communication.port=9300 #Tatami Bot configuration fr.ippon.tatami.bot.enabled=${fr.ippon.tatami.bot.enabled} #File & account size configuration #in bytes file.max.size=10000000 #in Mb storage.basic.max.size=10 storage.premium.max.size=1000 storage.ippon.max.size=100000 # #SubscriptionLevel suscription.level.free=0 suscription.level.premium=1 suscription.level.ippon=-1 ================================================ FILE: services/src/main/resources/ehcache.xml ================================================ ================================================ FILE: services/src/main/resources/logback.xml ================================================ utf-8 [%p] %c - %m%n ${logback.syslogHost} ${logback.syslogHost.port} LOCAL7 [%thread] %logger %msg ================================================ FILE: services/src/main/webapp/app/shared/services/AuthenticationService.js ================================================ TatamiApp.factory('AuthenticationService', ['$rootScope', '$state', 'UserSession', function($rootScope, $state, UserSession) { return { authenticate: function() { return UserSession.authenticate().then(function(result) { if(result !== null && result.action === null && $state.current.data && !$state.current.data.public) { // User isn't login in. Change the session token, and redirect to login UserSession.clearSession(); $state.go('tatami.login.main'); } }); } } }]); ================================================ FILE: services/src/main/webapp/app/shared/services/GeolocService.js ================================================ /** * This service is used to handle getting the geolocalisation of a user */ TatamiApp.factory('GeolocalisationService', function() { return { /** * Uses HTML5 to get geolocation information from the user * @returns If geolocation data can be found for the user * then we return the position in the form * "lat, lon" * Otherwise, we return the empty string. */ getGeolocalisation: function(callback) { if(navigator.geolocation){ navigator.geolocation.getCurrentPosition(function(position) { callback(position); }); } }, /** * Returns a string with the location data based on the openstreetmap website * @param position The users position * @returns {string} The URL to openstreetmaps */ getGeolocUrl: function(position) { var latitude = position.split(',')[0].trim(); var longitude = position.split(',')[1].trim(); return 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } }); ================================================ FILE: services/src/main/webapp/app/shared/services/GroupService.js ================================================ TatamiApp.factory('GroupService', ['$resource', function($resource) { return $resource('/tatami/rest/groups/:groupId', null, { 'getStatuses': { method: 'GET', isArray: true, params: { groupId: '@groupId' }, url: '/tatami/rest/groups/:groupId/timeline', transformResponse: function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = statuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + statuses[i].avatar + '/photo.jpg'; if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; } }, 'getMembers': { method: 'GET', isArray: true, params: { groupId: '@groupId' }, url: '/tatami/rest/groups/:groupId/members/', transformResponse: function(users) { users = angular.fromJson(users); for(var i = 0; i < users.length; i++) { users[i]['avatarURL'] = users[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + users[i].avatar + '/photo.jpg'; } return users; } }, 'getRecommendations': { method: 'GET', isArray: true, url: '/tatami/rest/groupmemberships/suggestions' }, 'join': { method: 'PUT', params: { groupId: '@groupId', username: '@username' }, url: '/tatami/rest/groups/:groupId/members/:username' }, 'leave': { method: 'DELETE', params: { groupId: '@groupId', username: '@username' }, url: '/tatami/rest/groups/:groupId/members/:username' }, 'update': { method: 'PUT', params: { groupId: '@groupId' }, url: '/tatami/rest/groups/:groupId' } }); }]); ================================================ FILE: services/src/main/webapp/app/shared/services/HomeService.js ================================================ TatamiApp.factory('HomeService', ['$resource', function($resource) { var responseTransform = function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = statuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + statuses[i].avatar + '/photo.jpg'; if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; }; return $resource(null, null, { 'getMentions': { method: 'GET', isArray: true, url: '/tatami/rest/mentions', transformResponse: responseTransform }, 'getFavorites': { method: 'GET', isArray: true, url: '/tatami/rest/favorites', transformResponse: responseTransform }, 'getCompanyTimeline': { method: 'GET', isArray: true, url: '/tatami/rest/company', transformResponse: responseTransform } }); }]); ================================================ FILE: services/src/main/webapp/app/shared/services/ProfileService.js ================================================ angular.module('TatamiApp.services', []) .factory('ProfileService', ['$resource', function ($resource) { return $resource('/tatami/rest/account/profile', null, { 'get': { method: 'GET', transformResponse: function (profile) { var parsedProfile = {}; try { parsedProfile = angular.fromJson(profile); } catch(e) { parsedProfile = {}; } parsedProfile['avatarURL'] = parsedProfile.avatar === '' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + parsedProfile.avatar + '/photo.jpg'; return parsedProfile; } }, 'update': { method: 'PUT', transformRequest: function (profile) { delete profile['avatarURL']; return angular.toJson(profile); } } }); }]); ================================================ FILE: services/src/main/webapp/app/shared/services/SearchService.js ================================================ TatamiApp.factory('SearchService', ['$resource', function($resource) { var responseTransform = function(users) { users = angular.fromJson(users); for(var i = 0; i < users.length; i++) { users[i]['avatarURL'] = users[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + users[i].avatar + '/photo.jpg'; } return users; }; return $resource('/tatami/rest/search/:term', { term: '@term' }, { 'query': { method: 'GET', isArray: true, transformResponse: responseTransform }, 'get': { method: 'GET', transformResponse: responseTransform } }); }]); ================================================ FILE: services/src/main/webapp/app/shared/services/StatusService.js ================================================ TatamiApp.factory('StatusService', ['$resource', function($resource) { var responseTransform = function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = statuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + statuses[i].avatar + '/photo.jpg'; if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; }; return $resource('/tatami/rest/statuses/:statusId', null, { 'get': { method: 'GET', transformResponse: function(status) { status = angular.fromJson(status); status.avatarURL = status.avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + status.avatar + '/photo.jpg'; if(status.geoLocalization) { var latitude = status.geoLocalization.split(',')[0].trim(); var longitude = status.geoLocalization.split(',')[1].trim(); status['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } return status; } }, 'getHomeTimeline': { method: 'GET', isArray: true, url: '/tatami/rest/statuses/home_timeline', transformResponse: responseTransform }, 'getUserTimeline': { method: 'GET', isArray: true, params: { username: '@username' }, url: '/tatami/rest/statuses/:username/timeline', transformResponse: responseTransform }, 'getDetails': { method: 'GET', params: { statusId: '@statusId' }, url: '/tatami/rest/statuses/details/:statusId', transformResponse: function(details) { details = angular.fromJson(details); for(var i = 0; i < details.discussionStatuses.length; i++) { details.discussionStatuses[i]['avatarURL'] = details.discussionStatuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + details.discussionStatuses[i].avatar + '/photo.jpg'; if(details.discussionStatuses[i].geoLocalization) { var latitude = details.discussionStatuses[i].geoLocalization.split(',')[0].trim(); var longitude = details.discussionStatuses[i].geoLocalization.split(',')[1].trim(); details.discussionStatuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } for(var i = 0; i < details.sharedByLogins.length; i++) { details.sharedByLogins[i]['avatarURL'] = details.sharedByLogins[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + details.sharedByLogins[i].avatar + '/photo.jpg'; } return details; } }, 'update': { method: 'PATCH', params: { statusId: '@statusId' } }, 'announce': { method: 'PATCH', params: { params: '@statusId' } } }); }]); ================================================ FILE: services/src/main/webapp/app/shared/services/TagService.js ================================================ TatamiApp.factory('TagService', ['$resource', function($resource) { return $resource('/tatami/rest/tags', null, { 'get': { method:'GET', params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag' }, 'getTagTimeline': { method:'GET', isArray: true, params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag/tag_timeline', transformResponse: function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = statuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + statuses[i].avatar + '/photo.jpg'; if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; } }, 'follow': { method:'PUT', params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag' }, 'getPopular': { method: 'GET', isArray: true, url: '/tatami/rest/tags/popular' } }); }]); ================================================ FILE: services/src/main/webapp/app/shared/services/TopPostersService.js ================================================ TatamiApp.factory('TopPostersService', ['$resource', function($resource) { return $resource('/tatami/rest/stats/day'); }]); ================================================ FILE: services/src/main/webapp/app/shared/services/UserService.js ================================================ TatamiApp.factory('UserService', ['$resource', function($resource) { var responseTransform = function(users) { users = angular.fromJson(users); for(var i = 0; i < users.length; i++) { users[i]['avatarURL'] = users[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + users[i].avatar + '/photo.jpg'; } return users; }; return $resource('/tatami/rest/users/:username', null, { 'get': { method: 'GET', params: { username: '@username' }, transformResponse: function(user) { user = angular.fromJson(user); user['avatarURL'] = user.avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + user.avatar + '/photo.jpg'; return user; } }, 'query': { method: 'GET', isArray: true, url: '/tatami/rest/users', transformResponse: responseTransform }, 'getFollowing': { method: 'GET', isArray: true, params: { username: '@username' }, url: '/tatami/rest/users/:username/friends', transformResponse: responseTransform }, 'getFollowers': { method: 'GET', isArray: true, params: { username: '@username' }, url: '/tatami/rest/users/:username/followers', transformResponse: responseTransform }, 'getSuggestions': { method: 'GET', isArray: true, url: '/tatami/rest/users/suggestions', transformResponse: function(suggestions) { suggestions = angular.fromJson(suggestions); for(var i = 0; i < suggestions.length; i++) { suggestions[i]['avatarURL'] = suggestions[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + suggestions[i].avatar + '/photo.jpg'; suggestions[i]['followingUser'] = false; } return suggestions; } }, 'follow': { method: 'PATCH', params: { username: '@username' } }, 'searchUsers': { method: 'GET', isArray: true, url: '/tatami/rest/users/:term', transformResponse: responseTransform }, 'deactivate': { method: 'PATCH', params: { username: '@username' } } }); }]); ================================================ FILE: services/src/main/webapp/app/shared/services/UserSession.js ================================================ TatamiApp.factory('UserSession', ['$q', '$window', 'ProfileService', 'localStorageService', function($q, $window, ProfileService, localStorageService) { var user; var authenticated = false; return { isAuthenticated: function() { return localStorageService.get('token') === "true"; }, isUserResolved: function() { return angular.isDefined(user); }, setLoginState: function(loggedIn) { localStorageService.set('token', loggedIn); }, clearSession: function() { localStorageService.clearAll(); }, getUser: function() { return user; }, authenticate: function(force) { var deferred = $q.defer(); if(force) { user = undefined; } if(this.isUserResolved() && user.action !== null) { deferred.resolve(user); return deferred.promise; } ProfileService.get(function(data) { // Success user = data; authenticated = true; deferred.resolve(user); }, function() { // Error user = null; authenticated = false; deferred.resolve(user); }); return deferred.promise; } } }]); ================================================ FILE: services/src/test/java/fr/ippon/tatami/AbstractCassandraTatamiTest.java ================================================ package fr.ippon.tatami; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.CounterRepository; import fr.ippon.tatami.service.util.DomainUtil; import fr.ippon.tatami.test.application.ApplicationTestConfiguration; import fr.ippon.tatami.test.application.WebApplicationTestConfiguration; import org.cassandraunit.DataLoader; import org.cassandraunit.dataset.json.ClassPathJsonDataSet; import org.cassandraunit.utils.EmbeddedCassandraServerHelper; import org.elasticsearch.client.Client; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.node.Node; import org.elasticsearch.node.NodeBuilder; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import javax.inject.Inject; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextHierarchy({ @ContextConfiguration( name = "root", classes = ApplicationTestConfiguration.class), @ContextConfiguration( name = "dispatcher", classes = WebApplicationTestConfiguration.class ) }) public abstract class AbstractCassandraTatamiTest { protected final Logger log = LoggerFactory.getLogger(this.getClass().getCanonicalName()); private static boolean isInitialized = false; private static final Object lock = new Object(); protected static Client client = null; @Inject private CounterRepository counterRepository; @BeforeClass public static void beforeClass() throws Exception { synchronized (lock) { if (!isInitialized) { EmbeddedCassandraServerHelper.startEmbeddedCassandra(); // create structure and load data String clusterName = "Tatami cluster"; String host = "localhost:9171"; DataLoader dataLoader = new DataLoader(clusterName, host); dataLoader.load(new ClassPathJsonDataSet("dataset/dataset.json")); final ImmutableSettings.Builder builder = ImmutableSettings.settingsBuilder(); builder.put("cluster.name", clusterName); final Node node = NodeBuilder.nodeBuilder().settings(builder.build()).local(true).node(); client = node.client(); isInitialized = true; } } } @AfterClass public static void afterClass() throws Exception { if (client != null) { client.close(); } } protected User constructAUser(String login, String firstName, String lastName) { User user = new User(); user.setLogin(login); user.setPassword(""); user.setUsername(DomainUtil.getUsernameFromLogin(login)); user.setDomain(DomainUtil.getDomainFromLogin(login)); user.setFirstName(firstName); user.setLastName(lastName); user.setJobTitle("web developer"); counterRepository.createStatusCounter(user.getLogin()); counterRepository.createFriendsCounter(user.getLogin()); counterRepository.createFollowersCounter(user.getLogin()); return user; } protected User constructAUser(String login) { return constructAUser(login, null, null); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/repository/MailDigestRepositoryTest.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.DigestType; import org.junit.Test; import javax.inject.Inject; import java.util.Calendar; import java.util.List; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** * @author Pierre Rust */ public class MailDigestRepositoryTest extends AbstractCassandraTatamiTest { @Inject public MailDigestRepository mailDigestRepository; @Test public void shouldGetAUserRepositoryInjected() { assertThat(mailDigestRepository, notNullValue()); } @Test public void shouldInsertWeeklySubscription() { log.debug("In shouldInsertWeeklySubscription"); String login = "nuuser@ippon.fr"; String domain = "ippon.fr"; String day = String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)); mailDigestRepository.subscribeToDigest(DigestType.WEEKLY_DIGEST, login, domain, day); List logins = mailDigestRepository.getLoginsRegisteredToDigest(DigestType.WEEKLY_DIGEST, domain, day, 0); assertThat(logins, notNullValue()); assertTrue(logins.contains(login)); } @Test public void shouldInsertDailySubscription() { log.debug("In shouldInsertDailySubscription"); String login = "nuuser@ippon.fr"; String domain = "ippon.fr"; String day = String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)); mailDigestRepository.subscribeToDigest(DigestType.DAILY_DIGEST, login, domain, day); List logins = mailDigestRepository.getLoginsRegisteredToDigest(DigestType.DAILY_DIGEST, domain, day, 0); assertThat(logins, notNullValue()); assertTrue(logins.contains(login)); } @Test public void shouldRemoveWeeklySubscription() { log.debug("In shouldRemoveWeeklySubscription"); String login = "nuuser@ippon.fr"; String domain = "ippon.fr"; String day = String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)); mailDigestRepository.unsubscribeFromDigest(DigestType.WEEKLY_DIGEST, login, domain, day); List logins = mailDigestRepository.getLoginsRegisteredToDigest(DigestType.WEEKLY_DIGEST, domain, day, 0); assertThat(logins, notNullValue()); assertTrue(!logins.contains(login)); } @Test public void shouldRemoveDailySubscription() { log.debug("In shouldRemoveDailySubscription"); String login = "nuuser@ippon.fr"; String domain = "ippon.fr"; String day = String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)); mailDigestRepository.unsubscribeFromDigest(DigestType.DAILY_DIGEST, login, domain, day); List logins = mailDigestRepository.getLoginsRegisteredToDigest(DigestType.DAILY_DIGEST, domain, day, 0); assertThat(logins, notNullValue()); assertTrue(!logins.contains(login)); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/repository/StatusReportRepositoryTest.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.AbstractCassandraTatamiTest; import org.junit.Test; import javax.inject.Inject; import java.util.Collection; import static junit.framework.TestCase.*; public class StatusReportRepositoryTest extends AbstractCassandraTatamiTest { @Inject public StatusReportRepository statusReportRepository; @Test public void reportNewStatus() { String reportingLogin = "user@localhost"; String reportedStatusId = "status-id"; String login = "emily@localhost"; String domain = "localhost"; int sizeOfReported = statusReportRepository.findReportedStatuses(domain).size(); statusReportRepository.reportStatus(domain, reportedStatusId, reportingLogin); Collection reportedStatuses = statusReportRepository.findReportedStatuses(domain); String reporter = statusReportRepository.findUserHavingReported(domain, reportedStatusId); //Now it should be equal to 1, since I added 1 reported status... assertEquals(sizeOfReported + 1, reportedStatuses.size()); assertEquals(reporter, reportingLogin); assertFalse(statusReportRepository.hasBeenReportedByUser(domain, reportedStatusId, login)); assertTrue(statusReportRepository.hasBeenReportedByUser(domain, reportedStatusId, reportingLogin)); sizeOfReported = statusReportRepository.findReportedStatuses(domain).size(); statusReportRepository.unreportStatus(domain, reportedStatusId); reportedStatuses = statusReportRepository.findReportedStatuses(domain); assertEquals(sizeOfReported - 1, reportedStatuses.size()); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/repository/StatusRepositoryTest.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.status.Status; import org.junit.Test; import javax.inject.Inject; import javax.validation.ConstraintViolationException; import javax.validation.ValidationException; import java.util.ArrayList; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; public class StatusRepositoryTest extends AbstractCassandraTatamiTest { @Inject public StatusRepository statusRepository; @Test public void shouldGetAStatusRepositoryInjected() { assertThat(statusRepository, notNullValue()); } @Test public void shouldCreateAStatus() { String login = "jdubois@ippon.fr"; String content = "content"; Status created = statusRepository.createStatus(login, false, null, new ArrayList(), content, "", "", "", "48.54654, 3.87987987"); assertThat(created, notNullValue()); } @Test(expected = ValidationException.class) public void shouldNotCreateAStatusBecauseLoginNull() { String login = null; String content = "content"; Status status = new Status(); status.setContent(content); status.setLogin(login); statusRepository.createStatus(login, false, null, new ArrayList(), content, "", "", "", null); } @Test(expected = ConstraintViolationException.class) public void shouldNotCreateAStatusBecauseContentNull() { String login = "jdubois@ippon.fr"; String username = "jdubois"; String domain = "ippon.fr"; String content = null; Status status = new Status(); status.setContent(content); status.setLogin(login); statusRepository.createStatus(login, false, null, new ArrayList(), content, "", "", "", null); } @Test(expected = ConstraintViolationException.class) public void shouldNotCreateAStatusBecauseContentEmpty() { String login = "jdubois@ippon.fr"; String username = "jdubois"; String domain = "ippon.fr"; String content = ""; Status status = new Status(); status.setContent(content); status.setLogin(login); statusRepository.createStatus(login, false, null, new ArrayList(), content, "", "", "", null); } @Test(expected = ConstraintViolationException.class) public void shouldNotCreateAStatusBecauseContentTooLarge() { String tmp = "0123456789"; String content = ""; for (int i = 0; i < 410; i++) { content += tmp; } Status status = new Status(); status.setContent(content); String login = "jdubois@ippon.fr"; String username = "jdubois"; String domain = "ippon.fr"; statusRepository.createStatus(login, false, null, new ArrayList(), content, "", "", "", null); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/repository/TagFollowerRepositoryTest.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.AbstractCassandraTatamiTest; import org.junit.Test; import javax.inject.Inject; import java.util.Collection; import static org.junit.Assert.assertEquals; public class TagFollowerRepositoryTest extends AbstractCassandraTatamiTest { @Inject public TagFollowerRepository tagFollowerRepository; @Test public void addNewFollowerForTag() { String domain = "ippon.fr"; String login = "jdubois@ippon.fr"; String tag = "tag"; Collection followers = tagFollowerRepository.findFollowers(domain, tag); assertEquals(0, followers.size()); tagFollowerRepository.addFollower(domain, tag, login); followers = tagFollowerRepository.findFollowers(domain, tag); assertEquals(1, followers.size()); assertEquals(login, followers.iterator().next()); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/repository/UserRepositoryTest.java ================================================ package fr.ippon.tatami.repository; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import org.junit.Test; import javax.inject.Inject; import javax.validation.ConstraintViolationException; import javax.validation.ValidationException; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; public class UserRepositoryTest extends AbstractCassandraTatamiTest { @Inject private UserRepository userRepository; @Inject private CounterRepository counterRepository; @Test public void shouldGetAUserRepositoryInjected() { assertThat(userRepository, notNullValue()); } @Test public void shouldCreateAUser() { String login = "nuuser@ippon.fr"; String firstName = "New"; String lastName = "User"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setPassword(""); user.setLastName(lastName); user.setAvatar(avatar); counterRepository.createStatusCounter(user.getLogin()); counterRepository.createFriendsCounter(user.getLogin()); counterRepository.createFollowersCounter(user.getLogin()); userRepository.createUser(user); assertThat(userRepository.findUserByLogin("nuuser@ippon.fr"), notNullValue()); } @Test(expected = ValidationException.class) public void shouldNotCreateAUserBecauseLoginNull() { String login = null; String firstName = "New"; String lastName = "User"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.createUser(user); } @Test(expected = ConstraintViolationException.class) public void shouldNotCreateAUserBecauseLoginEmpty() { String login = ""; String firstName = "New"; String lastName = "User"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.createUser(user); } @Test(expected = ValidationException.class) public void shouldNotUpdateAUserBecauseLoginNull() { String login = null; String firstName = "New"; String lastName = "User"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } @Test(expected = ConstraintViolationException.class) public void shouldNotUpdateAUserBecauseLoginEmpty() { String login = ""; String firstName = "New"; String lastName = "User"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } @Test(expected = ConstraintViolationException.class) public void shouldNotUpdateAUserBecauseEmailInvalid() { String login = "nuser_ippon.fr"; String firstName = "New"; String lastName = "User"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } @Test(expected = ValidationException.class) public void shouldNotUpdateAUserBecauseLastNameNull() { String login = "nuser_ippon.fr"; String firstName = "fs"; String lastName = null; String email = "nuser_ippon.fr"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } @Test(expected = ConstraintViolationException.class) public void shouldNotUpdateAUserBecauseLastNameEmpty() { String login = "nuser_ippon.fr"; String firstName = "eee"; String lastName = ""; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } @Test(expected = ConstraintViolationException.class) public void shouldNotUpdateAUserBecauseLastNameWithSeventeenCharacters() { String login = "nuser_ippon.fr"; String firstName = "eeee"; String lastName = "12345678901234567"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } @Test(expected = ValidationException.class) public void shouldNotUpdateAUserBecauseFirstNameNull() { String login = "nuser_ippon.fr"; String firstName = null; String lastName = "User"; String email = "nuser_ippon.fr"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } @Test(expected = ConstraintViolationException.class) public void shouldNotUpdateAUserBecauseFirstNameEmpty() { String login = "nuser_ippon.fr"; String firstName = ""; String lastName = "User"; String email = "nuser_ippon.fr"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } @Test(expected = ConstraintViolationException.class) public void shouldNotUpdateAUserBecauseFirstNameWithSeventeenCharacters() { String login = "nuser_ippon.fr"; String firstName = "12345678901234567"; String lastName = "User"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userRepository.updateUser(user); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/FriendshipServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class FriendshipServiceTest extends AbstractCassandraTatamiTest { @Inject public UserService userService; @Inject public FriendshipService friendshipService; @Test public void shouldGetAUserServiceInjected() { assertThat(userService, notNullValue()); } @Test public void shouldGetAFollowerServiceInjected() { assertThat(friendshipService, notNullValue()); } @Test public void shouldFollowUser() { mockAuthentication("userWhoWantToFollow@ippon.fr"); User userWhoWillBeFollowed = new User(); userWhoWillBeFollowed.setLogin("userWhoWillBeFollowed@ippon.fr"); userService.createUser(userWhoWillBeFollowed); userWhoWillBeFollowed.setDailyDigestSubscription(false); userWhoWillBeFollowed.setWeeklyDigestSubscription(false); userService.updateUser(userWhoWillBeFollowed); User userWhoFollow = userService.getUserByUsername("userWhoWantToFollow"); assertThat(userWhoFollow.getFriendsCount(), is(0L)); assertTrue(friendshipService.followUser("userWhoWillBeFollowed")); /* verify */ userWhoFollow = userService.getUserByUsername("userWhoWantToFollow"); assertThat(userWhoFollow.getFriendsCount(), is(1L)); User userWhoIsFollowed = userService.getUserByUsername("userWhoWillBeFollowed"); assertThat(userWhoIsFollowed.getFollowersCount(), is(1L)); // Clean up friendshipService.unfollowUser("userWhoWillBeFollowed"); } @Test public void shouldNotFollowUserBecauseUserDoesNotExist() { mockAuthentication("userWhoWantToFollow@ippon.fr"); assertFalse(friendshipService.followUser("unknownUser")); /* verify */ User userWhoFollow = userService.getUserByUsername("userWhoWantToFollow"); assertThat(userWhoFollow.getFriendsCount(), is(0L)); } @Test public void shouldNotFollowUserBecauseUserIsAlreadyFollowed() throws Exception { mockAuthentication("userWhoFollow@ippon.fr"); assertFalse(friendshipService.followUser("userWhoIsFollowed")); /* verify */ User userWhoFollow = userService.getUserByUsername("userWhoFollow"); assertThat(userWhoFollow.getFriendsCount(), is(1L)); assertThat(userWhoFollow.getFollowersCount(), is(0L)); User userWhoIsFollowed = userService.getUserByUsername("userWhoIsFollowed"); assertThat(userWhoIsFollowed.getFriendsCount(), is(0L)); assertThat(userWhoIsFollowed.getFollowersCount(), is(1L)); } @Test public void shouldNotFollowUserBecauseSameUser() throws Exception { mockAuthentication("userWhoWantToFollow@ippon.fr"); User userWhoFollow = userService.getUserByUsername("userWhoWantToFollow"); assertThat(userWhoFollow.getFriendsCount(), is(0L)); assertThat(userWhoFollow.getFollowersCount(), is(0L)); assertFalse(friendshipService.followUser("userWhoWantToFollow")); /* verify */ userWhoFollow = userService.getUserByUsername("userWhoWantToFollow"); assertThat(userWhoFollow.getFriendsCount(), is(0L)); assertThat(userWhoFollow.getFollowersCount(), is(0L)); } @Test public void shouldForgetUser() { mockAuthentication("userWhoWantToForget@ippon.fr"); User userWhoWantToForget = userService.getUserByUsername("userWhoWantToForget"); assertThat(userWhoWantToForget.getFriendsCount(), is(1L)); User userToForget = new User(); userToForget.setLogin("userToForget@ippon.fr"); userService.createUser(userToForget); userToForget.setDailyDigestSubscription(false); userToForget.setWeeklyDigestSubscription(false); userService.updateUser(userToForget); assertTrue(friendshipService.unfollowUser("userToForget")); userWhoWantToForget = userService.getUserByUsername("userWhoWantToForget"); assertThat(userWhoWantToForget.getFriendsCount(), is(0L)); User userWhoIsForgotten = userService.getUserByUsername("userToForget"); assertThat(userWhoIsForgotten.getFollowersCount(), is(0L)); } @Test public void shouldNotForgetUserBecauseUserDoesNotExist() { mockAuthentication("userWhoWantToForget@ippon.fr"); assertFalse(friendshipService.unfollowUser("unknownUser")); /* verify */ User userWhoWantToForget = userService.getUserByUsername("userWhoWantToForget"); assertThat(userWhoWantToForget.getFriendsCount(), is(0L)); } private void mockAuthentication(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(friendshipService, "authenticationService", mockAuthenticationService); ReflectionTestUtils.setField(userService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/GroupServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.dto.UserGroupDTO; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import java.util.Collection; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class GroupServiceTest extends AbstractCassandraTatamiTest { @Inject public UserService userService; @Inject public GroupService groupService; @Test public void createAndGetGroup() { mockAuthentication("uuser@ippon.fr"); User user = userService.getUserByLogin("uuser@ippon.fr"); String groupName = "Group name"; String groupDescription = "Group description"; boolean publicGroup = true; groupService.createGroup(groupName, groupDescription, publicGroup); Collection groups = groupService.getGroupsForUser(user); assertEquals(1, groups.size()); String groupId = groups.iterator().next().getGroupId(); Group group = groupService.getGroupById("ippon.fr", groupId); assertEquals(groupName, group.getName()); assertEquals(groupDescription, group.getDescription()); assertTrue(group.isPublicGroup()); assertFalse(group.isArchivedGroup()); } @Test public void createGroup() { mockAuthentication("jdubois@ippon.fr"); User user = userService.getUserByLogin("jdubois@ippon.fr"); assertEquals(0, groupService.getGroupsForUser(user).size()); String groupName = "Group name"; String groupDescription = "Group description"; boolean publicGroup = true; groupService.createGroup(groupName, groupDescription, publicGroup); Collection groups = groupService.getGroupsForUser(user); assertEquals(1, groups.size()); Group group = groups.iterator().next(); assertEquals(groupName, group.getName()); assertEquals(groupDescription, group.getDescription()); assertTrue(group.isPublicGroup()); assertFalse(group.isArchivedGroup()); } @Test public void addAndRemoveGroupMember() { mockAuthentication("userWithStatus@ippon.fr"); User user = userService.getUserByLogin("userWithStatus@ippon.fr"); assertEquals(0, groupService.getGroupsForUser(user).size()); String groupName = "Group name"; String groupDescription = "Group description"; boolean publicGroup = true; groupService.createGroup(groupName, groupDescription, publicGroup); Collection groups = groupService.getGroupsForUser(user); assertEquals(1, groups.size()); Group group = groups.iterator().next(); String groupId = group.getGroupId(); User member = userService.getUserByLogin("userWhoPostStatus@ippon.fr"); assertEquals(1, groupService.getGroupsForUser(user).size()); assertEquals(0, groupService.getGroupsForUser(member).size()); Collection members = groupService.getMembersForGroup(groupId, user.getLogin()); assertEquals(1, members.size()); groupService.addMemberToGroup(member, group); members = groupService.getMembersForGroup(groupId, user.getLogin()); assertEquals(2, members.size()); assertEquals(1, groupService.getGroupsForUser(user).size()); assertEquals(1, groupService.getGroupsForUser(member).size()); groupService.removeMemberFromGroup(member, group); members = groupService.getMembersForGroup(groupId, user.getLogin()); assertEquals(1, members.size()); assertEquals(1, groupService.getGroupsForUser(user).size()); assertEquals(0, groupService.getGroupsForUser(member).size()); // Clean up groupService.removeMemberFromGroup(user, group); assertEquals(0, groupService.getGroupsForUser(user).size()); } private void mockAuthentication(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(groupService, "authenticationService", mockAuthenticationService); ReflectionTestUtils.setField(userService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/MailDigestServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import org.junit.Before; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import java.util.ArrayList; import java.util.List; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyCollection; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyList; import static org.mockito.Mockito.*; /** * @author Pierre Rust */ @SuppressWarnings("unchecked") @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class MailDigestServiceTest extends AbstractCassandraTatamiTest { @Mock MailService mailServiceMock; // Note : see the mail text in the console, you need to comment out the InjectMocks annotation // the test will fail but it's still useful during development if you want to work on the template @Inject @InjectMocks public MailDigestService mailDigestService; @Inject public UserService userService; @Inject public TimelineService timelineService; @Inject public StatusUpdateService statusUpdateService; @Inject public FriendshipService friendshipService; static final String DAILY_DIGEST_USER = "userWhoSubscribeToDigests@ippon.fr"; static final String WEEKLY_DIGEST_USER = "userWhoSubscribeToWeeklyDigests@ippon.fr"; @Before public void initMocks() { // we need to call initMocks explicitly because we use // SpringJUnit4ClassRunner instead of MockitoJUnitRunner // (to get spring injection before mockito mocking with @InjectMocks) MockitoAnnotations.initMocks(this); // reset users registrations : mockAuthenticationOnUserService(DAILY_DIGEST_USER); userService.updateDailyDigestRegistration(false); mockAuthenticationOnUserService(WEEKLY_DIGEST_USER); userService.updateWeeklyDigestRegistration(false); } @Test public void order_00_shouldNotGenerateDailyDigest() { log.debug("In shouldNotGenerateDailyDigest"); // Test data set has no user subscribed to weekly Digest // no mail should be sent. mailDigestService.dailyDigest(); verifyZeroInteractions(mailServiceMock); } @Test public void order_01_shouldNotGenerateWeeklyDigest() { log.debug("In shouldNotGenerateWeeklyDigest"); // Test data set has no user subscribed to weekly Digest // no mail should be sent. mailDigestService.dailyDigest(); verifyZeroInteractions(mailServiceMock); } @Test public void order_03_shouldGenerateDailyDigestNoMessage() { log.debug("In shouldGenerateDailyDigestNoMessage"); mockAuthenticationOnUserService(DAILY_DIGEST_USER); userService.updateDailyDigestRegistration(true); ArgumentCaptor statuses = ArgumentCaptor.forClass(List.class); mailDigestService.dailyDigest(); verify(mailServiceMock).sendDailyDigestEmail(any(User.class), statuses.capture(), anyInt(), anyCollection()); assertThat(statuses.getValue().size() == 0, is(true)); } @Test public void order_04_shouldGenerateDailyDigestOneMessage() { log.debug("In shouldGenerateDailyDigestOneMessage"); mockAuthenticationOnUserService(DAILY_DIGEST_USER); userService.updateDailyDigestRegistration(true); mockAuthenticationOnFriendshipService(DAILY_DIGEST_USER); friendshipService.followUser("userWhoPostForDigests"); mockAuthenticationOnTimelineServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); mockAuthenticationOnStatusUpdateServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); String content = "voilà un message qui devrait se retrouver dans le digest ! "; statusUpdateService.postStatus(content, false, new ArrayList()); ArgumentCaptor statuses = ArgumentCaptor.forClass(List.class); mailDigestService.dailyDigest(); verify(mailServiceMock).sendDailyDigestEmail(any(User.class), statuses.capture(), anyInt(), anyCollection()); assertThat(statuses.getValue().size() == 1, is(true)); } @Test public void order_04_shouldGenerateDailyDigestWithTwoMessages() { log.debug("In shouldGenerateDailyDigestWithTwoMessages"); mockAuthenticationOnUserService(DAILY_DIGEST_USER); userService.updateDailyDigestRegistration(true); mockAuthenticationOnFriendshipService(DAILY_DIGEST_USER); friendshipService.followUser("userWhoPostForDigests"); mockAuthenticationOnTimelineServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); mockAuthenticationOnStatusUpdateServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); String content2 = "voilà un message 2 qui devrait se retrouver dans le digest ! "; statusUpdateService.postStatus(content2, false, new ArrayList()); ArgumentCaptor statuses = ArgumentCaptor.forClass(List.class); mailDigestService.dailyDigest(); verify(mailServiceMock).sendDailyDigestEmail(any(User.class), statuses.capture(), anyInt(), anyCollection()); assertThat(statuses.getValue().size() == 2, is(true)); } @Test public void order_05_shouldGenerateDailyDigestWithManyMessages() { log.debug("In shouldGenerateDailyDigestWithManyMessages"); mockAuthenticationOnUserService(DAILY_DIGEST_USER); userService.updateDailyDigestRegistration(true); mockAuthenticationOnFriendshipService(DAILY_DIGEST_USER); friendshipService.followUser("userWhoPostForDigests"); mockAuthenticationOnTimelineServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); mockAuthenticationOnStatusUpdateServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); for (int i = 0; i < 20; i++) { String content2 = "voilà un message " + i + " qui devrait se retrouver dans le digest ! "; statusUpdateService.postStatus(content2, false, new ArrayList()); } ArgumentCaptor statuses = ArgumentCaptor.forClass(List.class); mailDigestService.dailyDigest(); verify(mailServiceMock).sendDailyDigestEmail(any(User.class), statuses.capture(), anyInt(), anyCollection()); assertThat(statuses.getValue().size() == 10, is(true)); } @Test public void order_06_shouldGenerateWeeklyDigest() { log.debug("In shouldGenerateWeeklyDigest"); mockAuthenticationOnUserService(WEEKLY_DIGEST_USER); userService.updateWeeklyDigestRegistration(true); mailDigestService.weeklyDigest(); verify(mailServiceMock).sendWeeklyDigestEmail(any(User.class), anyList(), anyInt(), anyCollection(), anyCollection()); } @Test public void order_07_shouldGenerateWeeklyDigestNoMessage() { log.debug("In shouldGenerateWeeklyDigestNoMessage"); mockAuthenticationOnUserService(WEEKLY_DIGEST_USER); userService.updateWeeklyDigestRegistration(true); ArgumentCaptor statuses = ArgumentCaptor.forClass(List.class); mailDigestService.weeklyDigest(); verify(mailServiceMock).sendWeeklyDigestEmail(any(User.class), statuses.capture(), anyInt(), anyCollection(), anyCollection()); assertThat(statuses.getValue().size() == 0, is(true)); } @Test public void order_08_shouldGenerateWeeklyDigestOneMessage() { log.debug("In shouldGenerateWeeklyDigestOneMessage"); mockAuthenticationOnUserService(WEEKLY_DIGEST_USER); userService.updateWeeklyDigestRegistration(true); mockAuthenticationOnFriendshipService(WEEKLY_DIGEST_USER); friendshipService.followUser("userWhoPostForDigests"); mockAuthenticationOnTimelineServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); mockAuthenticationOnStatusUpdateServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); String content = "voilà un message qui devrait se retrouver dans le digest ! "; statusUpdateService.postStatus(content, false, new ArrayList()); ArgumentCaptor statuses = ArgumentCaptor.forClass(List.class); mailDigestService.weeklyDigest(); verify(mailServiceMock).sendWeeklyDigestEmail(any(User.class), statuses.capture(), anyInt(), anyCollection(), anyCollection()); assertThat(statuses.getValue().size() == 1, is(true)); } @Test public void order_09_shouldGenerateWeeklyDigestWithTwoMessages() { log.debug("In shouldGenerateWeeklyDigestWithTwoMessages"); mockAuthenticationOnUserService(WEEKLY_DIGEST_USER); userService.updateWeeklyDigestRegistration(true); mockAuthenticationOnFriendshipService(WEEKLY_DIGEST_USER); friendshipService.followUser("userWhoPostForDigests"); mockAuthenticationOnTimelineServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); mockAuthenticationOnStatusUpdateServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); String content2 = "voilà un message 2 qui devrait se retrouver dans le digest ! "; statusUpdateService.postStatus(content2, false, new ArrayList()); ArgumentCaptor statuses = ArgumentCaptor.forClass(List.class); mailDigestService.weeklyDigest(); verify(mailServiceMock).sendWeeklyDigestEmail(any(User.class), statuses.capture(), anyInt(), anyCollection(), anyCollection()); assertThat(statuses.getValue().size() == 2, is(true)); } @Test public void order_10_shouldGenerateWeeklyDigestWithManyMessages() { log.debug("In shouldGenerateWeeklyDigestWithManyMessages"); mockAuthenticationOnUserService(WEEKLY_DIGEST_USER); userService.updateWeeklyDigestRegistration(true); mockAuthenticationOnFriendshipService(WEEKLY_DIGEST_USER); friendshipService.followUser("userWhoPostForDigests"); mockAuthenticationOnTimelineServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); mockAuthenticationOnStatusUpdateServiceWithACurrentUser("userWhoPostForDigests@ippon.fr"); for (int i = 0; i < 20; i++) { String content2 = "voilà un message " + i + " qui devrait se retrouver dans le digest ! "; statusUpdateService.postStatus(content2, false, new ArrayList()); } ArgumentCaptor statuses = ArgumentCaptor.forClass(List.class); mailDigestService.weeklyDigest(); verify(mailServiceMock).sendWeeklyDigestEmail(any(User.class), statuses.capture(), anyInt(), anyCollection(), anyCollection()); assertThat(statuses.getValue().size() == 10, is(true)); } private void mockAuthenticationOnUserService(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(userService, "authenticationService", mockAuthenticationService); } private void mockAuthenticationOnFriendshipService(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(friendshipService, "authenticationService", mockAuthenticationService); ReflectionTestUtils.setField(userService, "authenticationService", mockAuthenticationService); } private void mockAuthenticationOnTimelineServiceWithACurrentUser(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(timelineService, "authenticationService", mockAuthenticationService); } private void mockAuthenticationOnStatusUpdateServiceWithACurrentUser(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(statusUpdateService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/StatsServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.UserStatusStat; import fr.ippon.tatami.security.AuthenticationService; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import java.util.Calendar; import java.util.Collection; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class StatsServiceTest extends AbstractCassandraTatamiTest { @Inject public StatsService statsService; @Test public void shouldGetDayline() throws Exception { mockAuthenticationOnStatsServiceWithACurrentUser("userWithStatus@ippon.fr"); Calendar cal = Calendar.getInstance(); cal.set(2012, 04, 19); Collection stats = statsService.getDayline(cal.getTime()); assertThat(stats, notNullValue()); assertThat(stats.size(), is(1)); UserStatusStat userStat = (UserStatusStat) stats.toArray()[0]; assertThat(userStat.getUsername(), is("userWithStatus")); assertThat(userStat.getStatusCount(), is(1L)); } private void mockAuthenticationOnStatsServiceWithACurrentUser(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(statsService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/StatusDeletionTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.dto.StatusDTO; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class StatusDeletionTest extends AbstractCassandraTatamiTest { private static final Logger log = LoggerFactory.getLogger(StatusDeletionTest.class); @Inject public TimelineService timelineService; @Inject public StatusUpdateService statusUpdateService; @Inject public GroupService groupService; @Inject public UserService userService; @Test public void deleteOneStatus() throws Exception { String login = "userWithStatus@ippon.fr"; String username = "userWithStatus"; mockAuthenticationOnTimelineServiceWithACurrentUser(login); Collection timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(2, timelineStatuses.size()); Collection userlineStatuses = timelineService.getUserline(username, 10, null, null); assertEquals(2, userlineStatuses.size()); String content = "temporary status"; statusUpdateService.postStatus(content, false, new ArrayList(), null); timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(3, timelineStatuses.size()); StatusDTO temporaryStatus = timelineStatuses.iterator().next(); assertEquals("temporary status", temporaryStatus.getContent()); userlineStatuses = timelineService.getUserline(username, 10, null, null); assertEquals(3, userlineStatuses.size()); timelineService.removeStatus(temporaryStatus.getStatusId()); timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(2, timelineStatuses.size()); userlineStatuses = timelineService.getUserline(username, 10, null, null); assertEquals(2, userlineStatuses.size()); } @Test public void deleteManyStatuses() throws Exception { String login = "userWithStatus@ippon.fr"; String username = "userWithStatus"; mockAuthenticationOnTimelineServiceWithACurrentUser(login); Collection timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(2, timelineStatuses.size()); Collection userlineStatuses = timelineService.getUserline(username, 10, null, null); assertEquals(2, userlineStatuses.size()); for (int i = 0; i < 10; i++) { String content = "temporary status " + i; statusUpdateService.postStatus(content, false, new ArrayList(),null); } timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(10, timelineStatuses.size()); userlineStatuses = timelineService.getUserline(username, 10, null, null); assertEquals(10, userlineStatuses.size()); Iterator iterator = timelineStatuses.iterator(); for (int i = 9; i >= 0; i--) { StatusDTO temporaryStatus = iterator.next(); assertEquals("temporary status " + i, temporaryStatus.getContent()); timelineService.removeStatus(temporaryStatus.getStatusId()); } timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(2, timelineStatuses.size()); userlineStatuses = timelineService.getUserline(username, 10, null, null); assertEquals(2, userlineStatuses.size()); } @Test public void deleteManyStatusesWithTag() throws Exception { String login = "userWithStatus@ippon.fr"; mockAuthenticationOnTimelineServiceWithACurrentUser(login); Collection tagStatuses = timelineService.getTagline("ippon", 10, null, null); assertEquals(2, tagStatuses.size()); for (int i = 0; i < 10; i++) { String content = "temporary status " + i + " #ippon"; statusUpdateService.postStatus(content, false, new ArrayList(),null); } tagStatuses = timelineService.getTagline("ippon", 10, null, null); assertEquals(10, tagStatuses.size()); Iterator iterator = tagStatuses.iterator(); for (int i = 9; i >= 0; i--) { StatusDTO temporaryStatus = iterator.next(); assertEquals("temporary status " + i + " #ippon", temporaryStatus.getContent()); timelineService.removeStatus(temporaryStatus.getStatusId()); } tagStatuses = timelineService.getTagline("ippon", 10, null, null); assertEquals(2, tagStatuses.size()); } @Test public void deleteManyStatusesInAGroup() throws Exception { String login = "uuser@ippon.fr"; mockAuthenticationOnTimelineServiceWithACurrentUser(login); User user = userService.getUserByLogin(login); int userGroupSize = groupService.getGroupsForUser(user).size(); String groupName = "Group with messages to delete"; String groupDescription = "Group description"; boolean publicGroup = true; groupService.createGroup(groupName, groupDescription, publicGroup); Collection groups = groupService.getGroupsForUser(user); assertEquals(userGroupSize + 1, groups.size()); Group group = groups.iterator().next(); Collection groupStatuses = timelineService.getGroupline(group.getGroupId(), 10, null, null); assertEquals(0, groupStatuses.size()); for (int i = 0; i < 12; i++) { String content = "temporary status " + i; statusUpdateService.postStatusToGroup(content, group, new ArrayList(), "1,2"); } groupStatuses = timelineService.getGroupline(group.getGroupId(), 10, null, null); assertEquals(10, groupStatuses.size()); Iterator iterator = groupStatuses.iterator(); for (int i = 11; i >= 2; i--) { StatusDTO temporaryStatus = iterator.next(); assertEquals("temporary status " + i, temporaryStatus.getContent()); timelineService.removeStatus(temporaryStatus.getStatusId()); } groupStatuses = timelineService.getGroupline(group.getGroupId(), 10, null, null); assertEquals(2, groupStatuses.size()); // Clean up groupService.removeMemberFromGroup(user, group); assertEquals(userGroupSize, groupService.getGroupsForUser(user).size()); } @Test public void deleteFavoriteStatuses() throws Exception { String login = "userWithStatus@ippon.fr"; mockAuthenticationOnTimelineServiceWithACurrentUser(login); Collection timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(2, timelineStatuses.size()); Collection favoriteStatuses = timelineService.getFavoritesline(); assertEquals(0, favoriteStatuses.size()); for (int i = 0; i < 10; i++) { String content = "temporary status " + i + " #ippon"; statusUpdateService.postStatus(content, false, new ArrayList(),null); } timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(10, timelineStatuses.size()); favoriteStatuses = timelineService.getFavoritesline(); assertEquals(0, favoriteStatuses.size()); Iterator iterator = timelineStatuses.iterator(); for (int i = 9; i >= 0; i--) { StatusDTO temporaryStatus = iterator.next(); timelineService.addFavoriteStatus(temporaryStatus.getStatusId()); } favoriteStatuses = timelineService.getFavoritesline(); assertEquals(10, favoriteStatuses.size()); iterator = timelineStatuses.iterator(); for (int i = 9; i >= 0; i--) { StatusDTO temporaryStatus = iterator.next(); assertEquals("temporary status " + i + " #ippon", temporaryStatus.getContent()); timelineService.removeStatus(temporaryStatus.getStatusId()); } timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(2, timelineStatuses.size()); favoriteStatuses = timelineService.getFavoritesline(); assertEquals(0, favoriteStatuses.size()); } @Test public void deleteMentionnedStatuses() throws Exception { String userWhoMentions = "userWithStatus@ippon.fr"; mockAuthenticationOnTimelineServiceWithACurrentUser(userWhoMentions); Collection timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(2, timelineStatuses.size()); String userWhoIsMentionned = "uuser@ippon.fr"; mockAuthenticationOnTimelineServiceWithACurrentUser(userWhoIsMentionned); Collection mentionStatuses = timelineService.getMentionline(10, null, null); assertEquals(0, mentionStatuses.size()); mockAuthenticationOnTimelineServiceWithACurrentUser(userWhoMentions); for (int i = 0; i < 10; i++) { String content = "Hello @uuser " + i; statusUpdateService.postStatus(content, false, new ArrayList(),null); } timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(10, timelineStatuses.size()); mockAuthenticationOnTimelineServiceWithACurrentUser(userWhoIsMentionned); mentionStatuses = timelineService.getMentionline(10, null, null); assertEquals(10, mentionStatuses.size()); mockAuthenticationOnTimelineServiceWithACurrentUser(userWhoMentions); Iterator iterator = timelineStatuses.iterator(); for (int i = 9; i >= 0; i--) { StatusDTO temporaryStatus = iterator.next(); assertEquals("Hello @uuser " + i, temporaryStatus.getContent()); timelineService.removeStatus(temporaryStatus.getStatusId()); } timelineStatuses = timelineService.getTimeline(10, null, null); assertEquals(2, timelineStatuses.size()); mockAuthenticationOnTimelineServiceWithACurrentUser(userWhoIsMentionned); mentionStatuses = timelineService.getMentionline(10, null, null); assertEquals(0, mentionStatuses.size()); } private void mockAuthenticationOnTimelineServiceWithACurrentUser(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(timelineService, "authenticationService", mockAuthenticationService); ReflectionTestUtils.setField(statusUpdateService, "authenticationService", mockAuthenticationService); ReflectionTestUtils.setField(groupService, "authenticationService", mockAuthenticationService); ReflectionTestUtils.setField(userService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/StatusUpdateServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.Status; import fr.ippon.tatami.repository.MentionlineRepository; import fr.ippon.tatami.repository.StatusRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.dto.StatusDTO; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.List; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class StatusUpdateServiceTest extends AbstractCassandraTatamiTest { @Inject public TimelineService timelineService; @Inject public StatusUpdateService statusUpdateService; @Inject public MentionlineRepository mentionlineRepository; @Inject public StatusRepository statusRepository; @Test public void shouldPostStatus() throws Exception { String username = "userWhoPostStatus"; mockAuthenticationOnTimelineServiceWithACurrentUser("userWhoPostStatus@ippon.fr"); mockAuthenticationOnStatusUpdateServiceWithACurrentUser("userWhoPostStatus@ippon.fr"); String content = "Longue vie au Ch'ti Jug"; statusUpdateService.postStatus(content, false, new ArrayList(), null); /* verify */ Collection statusFromUserline = timelineService.getUserline("userWhoPostStatus", 10, null, null); assertThatNewTestIsPosted(username, content, statusFromUserline); Collection statusFromTimeline = timelineService.getTimeline(10, null, null); assertThatNewTestIsPosted(username, content, statusFromTimeline); Collection statusFromUserlineOfAFollower = timelineService.getUserline("userWhoReadStatus", 10, null, null); assertThat(statusFromUserlineOfAFollower.isEmpty(), is(true)); } @Test public void shouldMentionUser() throws Exception { mockAuthenticationOnTimelineServiceWithACurrentUser("other@ippon.fr"); mockAuthenticationOnStatusUpdateServiceWithACurrentUser("other@ippon.fr"); String content = "Hello @jane! @john ! world"; statusUpdateService.postStatus(content, false, new ArrayList(), null); List janeMentions = mentionlineRepository.getMentionline("jane@ippon.fr", 10, null, null); assertThat(janeMentions.size(), is(1)); Status status = (Status) statusRepository.findStatusById(janeMentions.get(0)); assertThat(status.getContent(), is(content)); List johnMentions = mentionlineRepository.getMentionline("john@ippon.fr", 10, null, null); assertThat(johnMentions.size(), is(1)); } private void assertThatNewTestIsPosted(String username, String content, Collection statuses) { assertThat(statuses, notNullValue()); assertThat(statuses.size(), is(1)); StatusDTO status = (StatusDTO) statuses.toArray()[0]; assertThat(status.getUsername(), is(username)); assertThat(status.getContent(), is(content)); } private void mockAuthenticationOnTimelineServiceWithACurrentUser(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(timelineService, "authenticationService", mockAuthenticationService); } private void mockAuthenticationOnStatusUpdateServiceWithACurrentUser(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(statusUpdateService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/TagMembershipServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.web.rest.dto.Tag; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TagMembershipServiceTest extends AbstractCassandraTatamiTest { @Inject public UserService userService; @Inject public TagMembershipService tagMembershipService; @Test public void shouldFollowTag() { mockAuthentication("uuser@ippon.fr"); Tag tag = new Tag(); tag.setName("test"); assertTrue(tagMembershipService.followTag(tag)); assertTrue(tagMembershipService.unfollowTag(tag)); } @Test public void shouldNotFollowTagTwice() { mockAuthentication("uuser@ippon.fr"); Tag tag = new Tag(); tag.setName("test"); assertTrue(tagMembershipService.followTag(tag)); assertFalse(tagMembershipService.followTag(tag)); assertTrue(tagMembershipService.unfollowTag(tag)); } @Test public void shouldNotUnfollowUnknownTag() { mockAuthentication("uuser@ippon.fr"); Tag tag = new Tag(); tag.setName("test"); assertFalse(tagMembershipService.unfollowTag(tag)); } private void mockAuthentication(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(tagMembershipService, "authenticationService", mockAuthenticationService); ReflectionTestUtils.setField(userService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/TimelineServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.dto.StatusDTO; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import java.util.Collection; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TimelineServiceTest extends AbstractCassandraTatamiTest { @Inject public TimelineService timelineService; @Test public void shouldGetUserline() throws Exception { String username = "userWithStatus"; String login = "userWithStatus@ippon.fr"; mockAuthenticationOnTimelineServiceWithACurrentUser(login); Collection status = timelineService.getUserline(username, 10, null, null); assertThatLineForUserWithStatusIsOk(username, status); } @Test public void shouldGetAuthenticateUserUserlineWithNullLoginSet() throws Exception { String login = "userWithStatus@ippon.fr"; mockAuthenticationOnTimelineServiceWithACurrentUser("userWithStatus@ippon.fr"); Collection status = timelineService.getUserline(null, 10, null, null); assertThatLineForUserWithStatusIsOk("userWithStatus", status); } @Test public void shouldGetAuthenticateUserUserlineWithEmptyLoginSet() throws Exception { String username = "userWithStatus"; mockAuthenticationOnTimelineServiceWithACurrentUser("userWithStatus@ippon.fr"); Collection status = timelineService.getUserline("", 10, null, null); assertThatLineForUserWithStatusIsOk(username, status); } @Test public void shouldGetTimeline() throws Exception { String login = "userWithStatus@ippon.fr"; String username = "userWithStatus"; mockAuthenticationOnTimelineServiceWithACurrentUser(login); Collection status = timelineService.getTimeline(10, null, null); assertThatLineForUserWithStatusIsOk(username, status); } @Test public void shouldGetTagline() throws Exception { String login = "userWithStatus@ippon.fr"; String username = "userWithStatus"; mockAuthenticationOnTimelineServiceWithACurrentUser(login); String hashtag = "ippon"; Collection status = timelineService.getTagline(hashtag, 10, null, null); assertThatLineForUserWithStatusIsOk(username, status); } private void mockAuthenticationOnTimelineServiceWithACurrentUser(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(timelineService, "authenticationService", mockAuthenticationService); } private void assertThatLineForUserWithStatusIsOk(String username, Collection status) { assertThat(status, notNullValue()); assertThat(status.size(), is(2)); StatusDTO firstStatus = (StatusDTO) status.toArray()[0]; assertThat(firstStatus.getStatusId(), is("fa2bd770-9848-11e1-a6ca-e0f847068d52")); assertThat(firstStatus.getUsername(), is(username)); assertThat(firstStatus.getContent(), is("Tatami is an enterprise social network")); StatusDTO secondStatus = (StatusDTO) status.toArray()[1]; assertThat(secondStatus.getStatusId(), is("f97d6470-9847-11e1-a6ca-e0f847068d52")); assertThat(secondStatus.getUsername(), is(username)); assertThat(secondStatus.getContent(), is("Tatami is fully Open Source")); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/TrendServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.web.rest.dto.Trend; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TrendServiceTest extends AbstractCassandraTatamiTest { @Inject public StatusUpdateService statusUpdateService; @Inject public TrendService trendService; @Test public void testSearchTags() { mockAuthentication("currentuser@domain.com"); String domain = "domain.com"; Collection tags = trendService.searchTags(domain, "Te", 1); assertEquals(0, tags.size()); tags = trendService.searchTags(domain, "Test", 1); assertEquals(0, tags.size()); statusUpdateService.postStatus("Message #Test", false, new ArrayList(), null); tags = trendService.searchTags(domain, "Te", 1); assertEquals(1, tags.size()); tags = trendService.searchTags(domain, "Test", 1); assertEquals(1, tags.size()); } @Test public void testCurrentTrends() { mockAuthentication("currentuser@domain.com"); String domain = "domain.com"; Collection trends = trendService.getCurrentTrends(domain); for (Trend trend : trends) { if (trend.getTag().equals("TheTrend")) { fail("#TheTrend shoud not be trending yet"); } } for (int i = 0; i < 5; i++) { statusUpdateService.postStatus("Trending message " + i + " #Trending", false, new ArrayList(), null); } trends = trendService.getCurrentTrends(domain); boolean foundTrend = false; for (Trend trend : trends) { if (trend.getTag().equals("Trending")) { foundTrend = true; assertTrue(trend.isTrendingUp()); } } if (!foundTrend) { fail("#Trending should have been trending"); } for (int i = 0; i < 7; i++) { statusUpdateService.postStatus("New trending message " + i + " #NewTrend", false, new ArrayList(), null); } trends = trendService.getCurrentTrends(domain); foundTrend = false; boolean foundNewTrend = false; for (Trend trend : trends) { if (trend.getTag().equals("Trending")) { foundTrend = true; assertFalse(trend.isTrendingUp()); } else if (trend.getTag().equals("NewTrend")) { foundNewTrend = true; assertTrue(trend.isTrendingUp()); } } if (!foundTrend) { fail("#Trending should have been trending"); } if (!foundNewTrend) { fail("#NewTrend should have been trending"); } } @Test public void testUserTrends() { String login = "currentuser@domain.com"; mockAuthentication(login); Collection trends = trendService.getTrendsForUser(login); for (Trend trend : trends) { if (trend.getTag().equals("MyTrend")) { fail("#MyTrend shoud not be trending yet"); } } for (int i = 0; i < 5; i++) { statusUpdateService.postStatus("User trending message " + i + " #MyTrend", false, new ArrayList(), null); } trends = trendService.getTrendsForUser(login); boolean foundTrend = false; for (Trend trend : trends) { if (trend.getTag().equals("MyTrend")) { foundTrend = true; assertTrue(trend.isTrendingUp()); } } if (!foundTrend) { fail("#MyTrend should have been trending"); } } @Test public void testPrivateMessagesNotInTrends() { String login = "currentuser@domain.com"; mockAuthentication(login); for (int i = 0; i < 5; i++) { statusUpdateService.postStatus("@anotheruser private message " + i + " #NoTrend", true, new ArrayList(), null); } Collection trends = trendService.getCurrentTrends("domain.com"); boolean foundTrend = false; for (Trend trend : trends) { if (trend.getTag().equals("NoTrend")) { foundTrend = true; } } if (foundTrend) { fail("#NoTrend should not have been trending"); } trendService.getTrendsForUser(login); foundTrend = false; for (Trend trend : trends) { if (trend.getTag().equals("NoTrend")) { foundTrend = true; } } if (foundTrend) { fail("#NoTrend should not have been trending"); } } private void mockAuthentication(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(statusUpdateService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/UserServiceTest.java ================================================ package fr.ippon.tatami.service; import fr.ippon.tatami.AbstractCassandraTatamiTest; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.dto.UserDTO; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class UserServiceTest extends AbstractCassandraTatamiTest { @Inject public UserService userService; @Test public void shouldGetAUserServiceInjected() { assertThat(userService, notNullValue()); } @Test public void shouldGetAUserByLogin() { User user = userService.getUserByLogin("jdubois@ippon.fr"); assertThat(user, notNullValue()); assertThat(user.getAvatar(), is("avatar")); assertThat(user.getFirstName(), is("Julien")); assertThat(user.getLastName(), is("Dubois")); } @Test public void shouldNotGetAUserByLogin() { User user = userService.getUserByLogin("unknownUserLogin"); assertThat(user, nullValue()); } @Test public void shouldGetAUserProfileByLogin() { mockAuthenticationOnUserService("jdubois@ippon.fr"); User user = userService.getUserByUsername("jdubois"); assertThat(user.getStatusCount(), is(2L)); assertThat(user.getFollowersCount(), is(3L)); assertThat(user.getFriendsCount(), is(4L)); } @Test public void shouldNotGetAUserProfileByLogin() { User user = userService.getUserByUsername("unknownUserLogin"); assertThat(user, nullValue()); } @Test public void shouldUpdateUser() { String login = "uuser@ippon.fr"; String firstName = "UpdatedFirstName"; String lastName = "UpdatedLastName"; User userToUpdate = constructAUser(login, firstName, lastName); mockAuthenticationOnUserService(login); userService.updateUser(userToUpdate); User updatedUser = userService.getUserByLogin(login); assertThat(updatedUser.getFirstName(), is(firstName)); assertThat(updatedUser.getLastName(), is(lastName)); } @Test public void createUserWithUsernameAndDomain() { mockAuthenticationOnUserService("currentuser@domain.com"); String login = "username@domain.com"; User user = new User(); user.setLogin(login); userService.createUser(user); User createdUser = userService.getUserByUsername("username"); assertThat(createdUser.getUsername(), is("username")); assertThat(createdUser.getDomain(), is("domain.com")); assertNotNull(createdUser.getPassword()); assertThat(createdUser.getPassword().length(), is(80)); // Size of the encrypted password } @Test public void shouldCreateAUser() { mockAuthenticationOnUserService("currentuser@ippon.fr"); String login = "nuser@ippon.fr"; String firstName = "New"; String lastName = "User"; String avatar = "newAvatar"; User user = new User(); user.setLogin(login); user.setFirstName(firstName); user.setLastName(lastName); user.setAvatar(avatar); userService.createUser(user); /* verify */ User userToBeTheSame = userService.getUserByUsername("nuser"); assertThat(userToBeTheSame.getLogin(), is(user.getLogin())); assertThat(userToBeTheSame.getFirstName(), is(user.getFirstName())); assertThat(userToBeTheSame.getLastName(), is(user.getLastName())); assertThat(userToBeTheSame.getAvatar(), is(user.getAvatar())); assertThat(userToBeTheSame.getStatusCount(), is(0L)); assertThat(userToBeTheSame.getFollowersCount(), is(0L)); assertThat(userToBeTheSame.getFriendsCount(), is(0L)); } @Test public void shouldRegisterUserToWeeklyEmailDigest() { String login = "uuser@ippon.fr"; mockAuthenticationOnUserService(login); userService.updateWeeklyDigestRegistration(true); User updatedUser = userService.getUserByLogin(login); assertTrue(updatedUser.getWeeklyDigestSubscription()); userService.updateWeeklyDigestRegistration(false); updatedUser = userService.getUserByLogin(login); assertFalse(updatedUser.getWeeklyDigestSubscription()); } @Test public void shouldRegisterUserToDailyEmailDigest() { String login = "uuser@ippon.fr"; mockAuthenticationOnUserService(login); userService.updateDailyDigestRegistration(true); User updatedUser = userService.getUserByLogin(login); assertTrue(updatedUser.getDailyDigestSubscription()); userService.updateDailyDigestRegistration(false); updatedUser = userService.getUserByLogin(login); assertFalse(updatedUser.getDailyDigestSubscription()); } @Test public void testGetUsersByLogin() { String login1 = "uuser@ippon.fr"; String login2 = "jdubois@ippon.fr"; Collection logins = new ArrayList(); logins.add(login1); logins.add(login2); mockAuthenticationOnUserService(login2); Collection users = userService.getUsersByLogin(logins); assertEquals(2, users.size()); } @Test public void testGetUsersForCurrentDomain() { mockAuthenticationOnUserService("jdubois@ippon.fr"); Collection users = userService.getUsersForCurrentDomain(0); assertTrue(users.size() > 10); } @Test public void testUpdatePassword() { String login = "jdubois@ippon.fr"; mockAuthenticationOnUserService(login); User testUser = userService.getUserByLogin(login); assertNull(testUser.getPassword()); testUser.setPassword("newPassword"); userService.updatePassword(testUser); testUser = userService.getUserByLogin(login); assertNotNull(testUser.getPassword()); assertNotEquals("newPassword", testUser.getPassword()); } @Test public void testBuildUserDTOList() { String login = "jdubois@ippon.fr"; mockAuthenticationOnUserService(login); User testUser = userService.getUserByLogin(login); Collection users = new ArrayList(); users.add(testUser); Collection userDTOs = userService.buildUserDTOList(users); assertEquals(1, userDTOs.size()); UserDTO dto = userDTOs.iterator().next(); assertEquals("Julien", dto.getFirstName()); assertEquals(3, dto.getFollowersCount()); assertEquals(4, dto.getFriendsCount()); } private void mockAuthenticationOnUserService(String login) { User authenticateUser = constructAUser(login); AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); ReflectionTestUtils.setField(userService, "authenticationService", mockAuthenticationService); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/service/elasticsearch/ElasticsearchSearchServiceTest.java ================================================ //package fr.ippon.tatami.service.elasticsearch; // //import fr.ippon.tatami.AbstractCassandraTatamiTest; //import fr.ippon.tatami.service.AdminService; //import fr.ippon.tatami.service.SearchService; //import org.junit.Test; //import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; //import org.springframework.security.core.Authentication; //import org.springframework.security.core.GrantedAuthority; //import org.springframework.security.core.authority.SimpleGrantedAuthority; //import org.springframework.security.core.context.SecurityContextHolder; // //import javax.inject.Inject; //import java.util.ArrayList; //import java.util.Collection; // //import static org.junit.Assert.assertEquals; // //public class ElasticsearchSearchServiceTest extends AbstractCassandraTatamiTest { // // @Inject // private AdminService adminService; // // @Inject // private SearchService searchService; // // @Test // public void resetElasticSearch() throws InterruptedException { // // The user needs to have the admin role // GrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN"); // Collection grantedAuthorities = new ArrayList(); // grantedAuthorities.add(adminAuthority); // // org.springframework.security.core.userdetails.User userDetails = // new org.springframework.security.core.userdetails.User("tatami@ippon.fr", "", grantedAuthorities); // // Authentication authentication = // new UsernamePasswordAuthenticationToken(userDetails, // userDetails.getPassword(), // userDetails.getAuthorities()); // // SecurityContextHolder.getContext().setAuthentication(authentication); // // // Clean the index if needed // searchService.reset(); // // // Test the user index // Collection users = searchService.searchUserByPrefix("ippon.fr", "jdub"); // assertEquals(0, users.size()); // // adminService.rebuildIndex(); // // // Test every 100ms, for 30 seconds : this is the time for Elastic Search to index everything // for (int i = 0; i < 100; i++) { // Thread.sleep(300 * i); // users = searchService.searchUserByPrefix("ippon.fr", "jdub"); // if (users.size() > 0) { // break; // } // } // // assertEquals(1, users.size()); // assertEquals("jdubois@ippon.fr", users.iterator().next()); // } //} ================================================ FILE: services/src/test/java/fr/ippon/tatami/test/MockUtils.java ================================================ package fr.ippon.tatami.test; import org.mockito.internal.util.MockUtil; import java.util.concurrent.Callable; public class MockUtils { /** * @param mock a Mockito mock * @return */ public static int getNumberOfInvocation(Object mock) { // WARNING : we use internal mockito code here : return new MockUtil().getMockHandler(mock).getInvocationContainer().getInvocations().size(); } /** * To be used with Awaitility.await().until(xxx) * * @param mock * @param minInvocationCount * @return */ public static Callable mockCalledCallable(final Object mock, final int minInvocationCount) { return new Callable() { public Boolean call() throws Exception { int nbCalls = getNumberOfInvocation(mock); return nbCalls >= minInvocationCount; } }; } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/test/application/ApplicationTestConfiguration.java ================================================ package fr.ippon.tatami.test.application; import fr.ippon.tatami.config.AsyncConfiguration; import fr.ippon.tatami.config.CassandraConfiguration; import fr.ippon.tatami.config.MailConfiguration; import fr.ippon.tatami.config.SearchConfiguration; import org.apache.thrift.transport.TTransportException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.*; import javax.annotation.PostConstruct; import java.io.IOException; @Configuration @PropertySource("classpath:/tatami/tatami-test.properties") @ComponentScan(basePackages = {"fr.ippon.tatami.repository", "fr.ippon.tatami.service", "fr.ippon.tatami.security"}) @Import(value = {AsyncConfiguration.class, CassandraConfiguration.class, SearchConfiguration.class, MailConfiguration.class}) @ImportResource({"classpath:META-INF/spring/applicationContext-security.xml"}) public class ApplicationTestConfiguration { private final Logger log = LoggerFactory.getLogger(ApplicationTestConfiguration.class); @PostConstruct public void initTatami() throws IOException, TTransportException { this.log.info("Tatami test context started!"); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/test/application/WebApplicationTestConfiguration.java ================================================ package fr.ippon.tatami.test.application; import fr.ippon.tatami.config.DispatcherServletConfig; import org.apache.thrift.transport.TTransportException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.test.context.web.WebAppConfiguration; import javax.annotation.PostConstruct; import java.io.IOException; @WebAppConfiguration @Configuration @Import(value = {DispatcherServletConfig.class}) public class WebApplicationTestConfiguration { private final Logger log = LoggerFactory.getLogger(WebApplicationTestConfiguration.class); @PostConstruct public void initTatami() throws IOException, TTransportException { this.log.info("Tatami Web test context started!"); } } ================================================ FILE: services/src/test/java/fr/ippon/tatami/web/syndic/SyndicTimelineControllerTest.java ================================================ //package fr.ippon.tatami.web.syndic; // //import com.fasterxml.jackson.databind.ObjectMapper; //import fr.ippon.tatami.AbstractCassandraTatamiTest; //import fr.ippon.tatami.domain.User; //import fr.ippon.tatami.security.AuthenticationService; //import fr.ippon.tatami.service.StatusUpdateService; //import fr.ippon.tatami.service.TimelineService; //import fr.ippon.tatami.service.UserService; //import fr.ippon.tatami.service.dto.StatusDTO; //import fr.ippon.tatami.web.rest.AccountController; //import fr.ippon.tatami.web.rest.TimelineController; //import fr.ippon.tatami.web.rest.dto.Preferences; //import org.apache.commons.lang.CharEncoding; //import org.junit.Before; //import org.junit.Test; //import org.springframework.context.i18n.LocaleContextHolder; //import org.springframework.context.support.ReloadableResourceBundleMessageSource; //import org.springframework.core.env.Environment; //import org.springframework.http.MediaType; //import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; //import org.springframework.security.core.Authentication; //import org.springframework.security.core.GrantedAuthority; //import org.springframework.security.core.context.SecurityContextHolder; //import org.springframework.test.util.ReflectionTestUtils; //import org.springframework.test.web.servlet.MockMvc; //import org.springframework.test.web.servlet.setup.MockMvcBuilders; //import org.springframework.web.servlet.ModelAndView; // //import javax.inject.Inject; //import java.util.ArrayList; //import java.util.Collection; //import java.util.Locale; // //import static org.junit.Assert.assertEquals; //import static org.mockito.Mockito.mock; //import static org.mockito.Mockito.when; //import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; //import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; //import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; // //public class SyndicTimelineControllerTest extends AbstractCassandraTatamiTest { // // @Inject // private TimelineService timelineService; // // @Inject // private StatusUpdateService statusUpdateService; // // @Inject // private UserService userService; // // @Inject // Environment env; // // private MockMvc mockMvc; // // private MockMvc timelineMockMvc; // // private MockMvc accountMockMvc; // // private static final String username = "timelineUser"; // // @Before // public void setup() { // // TimelineController timelineController = new TimelineController(); // ReflectionTestUtils.setField(timelineController, "timelineService", timelineService); // ReflectionTestUtils.setField(timelineController, "statusUpdateService", statusUpdateService); // // User authenticateUser = constructAUser(username + "@ippon.fr"); // AuthenticationService mockAuthenticationService = mock(AuthenticationService.class); // when(mockAuthenticationService.getCurrentUser()).thenReturn(authenticateUser); // ReflectionTestUtils.setField(timelineController, "authenticationService", mockAuthenticationService); // ReflectionTestUtils.setField(userService, "authenticationService", mockAuthenticationService); // ReflectionTestUtils.setField(timelineService, "authenticationService", mockAuthenticationService); // ReflectionTestUtils.setField(statusUpdateService, "authenticationService", mockAuthenticationService); // this.timelineMockMvc = MockMvcBuilders.standaloneSetup(timelineController).build(); // // AccountController accountController = new AccountController(); // ReflectionTestUtils.setField(accountController, "userService", userService); // ReflectionTestUtils.setField(accountController, "env", env); // ReflectionTestUtils.setField(accountController, "authenticationService", mockAuthenticationService); // this.accountMockMvc = MockMvcBuilders.standaloneSetup(accountController).build(); // // SyndicTimelineController syndicTimelineController = new SyndicTimelineController(); // ReflectionTestUtils.setField(syndicTimelineController, "timelineService", timelineService); // ReflectionTestUtils.setField(syndicTimelineController, "userService", userService); // // ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); // messageSource.setBasename("file:src/main/webapp/WEB-INF/messages/messages"); // messageSource.setDefaultEncoding(CharEncoding.UTF_8); // ReflectionTestUtils.setField(syndicTimelineController, "messageSource", messageSource); // this.mockMvc = MockMvcBuilders.standaloneSetup(syndicTimelineController).build(); // } // // @Test // @SuppressWarnings("unchecked") // public void testStatusUpdate() throws Exception { // LocaleContextHolder.setLocale(Locale.US); // // // Post content // timelineMockMvc.perform(post("/rest/statuses/") // .contentType(MediaType.APPLICATION_JSON) // .content("{\"content\":\"Test status for RSS syndication\"}")) // .andExpect(status().isOk()); // // // Get a RSS stream that is not correct // // mockMvc.perform(get("/syndic/12345")) // .andExpect(status().isNotFound()); // // // Enable RSS for this user // org.springframework.security.core.userdetails.User userDetails = // new org.springframework.security.core.userdetails.User(username, "", new ArrayList()); // // Authentication authentication = // new UsernamePasswordAuthenticationToken(userDetails, // userDetails.getPassword(), // userDetails.getAuthorities()); // // SecurityContextHolder.getContext().setAuthentication(authentication); // accountMockMvc.perform(post("/rest/account/preferences") // .contentType(MediaType.APPLICATION_JSON) // .content("{\"mentionEmail\":true," + // "\"weeklyDigest\":false," + // "\"dailyDigest\":false," + // "\"rssUidActive\":true," + // "\"rssUid\":\"\"}")) // .andExpect(status().isOk()); // // //Get RSS ID // String preferencesAsJson = accountMockMvc.perform(get("/rest/account/preferences") // .accept(MediaType.APPLICATION_JSON)) // .andExpect(status().isOk()) // .andExpect(content().contentType("application/json")) // .andExpect(jsonPath("$.rssUidActive").value(true)) // .andReturn().getResponse().getContentAsString(); // // Preferences preferences = new ObjectMapper().readValue(preferencesAsJson, Preferences.class); // // String rssId = preferences.getRssUid(); // // ModelAndView result = mockMvc.perform(get("/syndic/" + rssId)) // .andExpect(status().isOk()) // .andReturn().getModelAndView(); // // Collection statuses = (Collection) result.getModel().get("feedContent"); // assertEquals("Test status for RSS syndication", statuses.iterator().next().getContent()); // // } //} ================================================ FILE: services/src/test/jmeter/tatami-create-users.jmx ================================================ Use the "stress-test" Maven profile to use this plan false false continue false 1 1 1 1333013329000 1333013329000 false userNumber 199 = followerNumber 199 = 127.0.0.1 8080 /tatami Java 4 false true ${__BeanShell(Integer.parseInt(vars.get("userNumber"))+1)} 0 ${userNumber} 1 userCount true true false user${userCount}@test.com = true email false test = true password /tatami/register/automatic POST true false true false false true ${__BeanShell(Integer.parseInt(vars.get("followerNumber"))+1)} 0 ${followerNumber} 1 followerCount true true false userfollower${followerCount}@test.com = true email false test = true password /tatami/register/automatic POST true false true false false false userfollower${followerCount}@test.com = true j_username false test = true j_password /tatami/authentication POST true false true false false true ${__BeanShell(Integer.parseInt(vars.get("userNumber"))+1)} 0 ${userNumber} 1 userToFollowCount true true true false {"username":"user${userToFollowCount}"} = /tatami/rest/friendships/create POST true false true false false Content-Type application/json; charset=UTF-8 Accept application/json /tatami/logout GET true false true false false false saveConfig true true true true true true true false true true false false true false false false false false 0 true false saveConfig true true true true true true true false true true false false false false false false false false 0 true ================================================ FILE: services/src/test/jmeter/tatami-stress-test.jmx ================================================ Use the "stress-test" Maven profile to use this plan false false continue false 20 200 10 1333013329000 1333013329000 false 300 100.0 0 200 1 userCount false 127.0.0.1 8080 /tatami Java 4 false /tatami/login GET true false true false false false user${userCount}@test.com = true email false test = true password /tatami/register/automatic POST true false true false false false user${userCount}@test.com = true j_username false test = true j_password /tatami/authentication POST true false true false false /tatami/ GET true false true false false /tatami/rest/account/profile GET true false true false false /tatami/rest/trends GET true false true false false /tatami/rest/groupmemberships/lookup?screen_name=user${userCount} GET true false true false false /tatami/rest/users/show?screen_name=user${userCount} GET true false true false false /tatami/rest/users/suggestions GET true false true false false /tatami/rest/statuses/home_timeline GET true false true false false true 5 0 1 statusCount true true true false {"content":"test status ${userCount} - ${statusCount} #tag @user0"} = /tatami/rest/statuses/update POST true false true false false Content-Type application/json; charset=UTF-8 Accept application/json true 10 /tatami/rest/statuses/home_timeline GET true false true false false false saveConfig true true true true true true true false true true false false true false false false false false 0 true false saveConfig true true true true true true true false true true false false false false false false false false 0 true ================================================ FILE: services/src/test/resources/dataset/dataset.json ================================================ { "name":"tatami", "columnFamilies":[ { "name":"User", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"UTF8Type", "rows":[ { "key":"jdubois@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"Julien" }, { "name":"lastName", "value":"Dubois" }, { "name":"username", "value":"jdubois" }, { "name":"domain", "value":"ippon.fr" } ] }, { "key":"uuser@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"Update" }, { "name":"lastName", "value":"User" }, { "name":"username", "value":"uuser" }, { "name":"domain", "value":"ippon.fr" } ] }, { "key":"timelineUser@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"TimelineUser" }, { "name":"username", "value":"timelineUser" }, { "name":"domain", "value":"ippon.fr" } ] }, { "key":"userWhoHasGroup@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoHasGroup" }, { "name":"username", "value":"userWhoHasGroup" }, { "name":"domain", "value":"ippon.fr" } ] }, { "key":"userWhoWantToFollow@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoWantToFollow" } ] }, { "key":"userWhoWillBeFollowed@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoWillBeFollowed" } ] }, { "key":"userWhoFollow@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoFollow" } ] }, { "key":"userWhoIsFollowed@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoIsFollowed" } ] }, { "key":"userWhoWantToForget@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoWantToForget" } ] }, { "key":"userToForget@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"ToForget" } ] }, { "key":"userWithStatus@ippon.fr", "columns":[ { "name":"username", "value":"userWithStatus" }, { "name":"domain", "value":"ippon.fr" }, { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WithUserline" } ] }, { "key":"userWhoPostStatus@ippon.fr", "columns":[ { "name":"username", "value":"userWhoPostStatus" }, { "name":"domain", "value":"ippon.fr" }, { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoPostStatus" } ] }, { "key":"userWhoReadStatus@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoReadStatus" } ] }, { "key":"userWhoShouldBeFoundBySimilarSearch@ippon.fr", "columns":[ { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoShouldBeFoundBySimilarSearch" } ] }, { "key":"userWhoSubscribeToDigests@ippon.fr", "columns":[ { "name":"username", "value":"userWhoSubscribeToDigests" }, { "name":"domain", "value":"ippon.fr" }, { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoSubscribeToDigests" } ] }, { "key":"userWhoPostForDigests@ippon.fr", "columns":[ { "name":"username", "value":"userWhoPostForDigests" }, { "name":"domain", "value":"ippon.fr" }, { "name":"avatar", "value":"avatar" }, { "name":"firstName", "value":"User" }, { "name":"lastName", "value":"WhoPostForDigests" } ] } ] }, { "name":"Domain", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "rows" : [ { "key":"ippon.fr", "columns":[ { "name":"jdubois@ippon.fr", "value":"" }, { "name":"uuser@ippon.fr", "value":"" }, { "name":"timelineUser@ippon.fr", "value":"" }, { "name":"userWhoHasGroup@ippon.fr", "value":"" }, { "name":"userWhoWantToFollow@ippon.fr", "value":"" }, { "name":"userWhoWillBeFollowed@ippon.fr", "value":"" }, { "name":"userWhoFollow@ippon.fr", "value":"" }, { "name":"userWhoIsFollowed@ippon.fr", "value":"" }, { "name":"userWhoWantToForget@ippon.fr", "value":"" }, { "name":"userToForget@ippon.fr", "value":"" }, { "name":"userWithStatus@ippon.fr", "value":"" }, { "name":"userWhoPostStatus@ippon.fr", "value":"" }, { "name":"userWhoReadStatus@ippon.fr", "value":"" }, { "name":"userWhoShouldBeFoundBySimilarSearch@ippon.fr", "value":"" }, { "name": "userWhoSubscribeToDigests@ippon.fr", "value": "" }, { "name":"userWhoPostForDigests@ippon.fr", "value" : "" } ] } ] }, { "name" : "MailDigest", "keyType":"UTF8Type", "comparatorType":"UTF8Type" }, { "name":"Counter", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"CounterColumnType", "rows":[ { "key":"jdubois@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"2" }, { "name":"FOLLOWERS_COUNTER", "value":"3" }, { "name":"FRIENDS_COUNTER", "value":"4" } ] }, { "key":"timelineUser@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWhoHasGroup@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWhoWantToFollow@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWhoWillBeFollowed@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWhoFollow@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"1" } ] }, { "key":"userWhoIsFollowed@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"1" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWhoWantToForget@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"1" } ] }, { "key":"userToForget@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"1" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWithStatus@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"2" }, { "name":"FOLLOWERS_COUNTER", "value":"1" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWhoPostStatus@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"1" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWhoReadStatus@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"1" } ] }, { "key":"userWhoSubscribeToDigests@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] }, { "key":"userWhoPostForDigests@ippon.fr", "columns":[ { "name":"STATUS_COUNTER", "value":"0" }, { "name":"FOLLOWERS_COUNTER", "value":"0" }, { "name":"FRIENDS_COUNTER", "value":"0" } ] } ] }, { "name":"Friends", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"LongType", "rows":[ { "key":"userWhoFollow@ippon.fr", "columns":[ { "name":"userWhoIsFollowed@ippon.fr", "value":"0" } ] }, { "key":"userWhoWantToForget@ippon.fr", "columns":[ { "name":"userToForget@ippon.fr", "value":"0" } ] }, { "key":"userWhoReadStatus@ippon.fr", "columns":[ { "name":"userWhoPostStatus@ippon.fr", "value":"0" } ] } ] }, { "name":"Followers", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"LongType", "rows":[ { "key":"userWhoPostStatus@ippon.fr", "columns":[ { "name":"userWhoReadStatus@ippon.fr", "value":"0" } ] } ] }, { "name":"Favline", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type", "rows":[ { "key":"userWhoPostStatus@ippon.fr" } ] }, { "name":"Dayline", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"CounterColumnType", "rows":[ { "key":"19052012-ippon.fr", "columns":[ { "name":"userWithStatus", "value":"1" } ] } ] }, { "name":"Tagline", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type", "rows":[ { "key":"ippon-ippon.fr", "columns":[ { "name":"fa2bd770-9848-11e1-a6ca-e0f847068d52", "value":"" }, { "name":"f97d6470-9847-11e1-a6ca-e0f847068d52", "value":"" } ] } ] }, { "name":"Timeline", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type", "rows":[ { "key":"userWithStatus@ippon.fr", "columns":[ { "name":"fa2bd770-9848-11e1-a6ca-e0f847068d52", "value":"" }, { "name":"f97d6470-9847-11e1-a6ca-e0f847068d52", "value":"" } ] } ] }, { "name":"Userline", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type", "rows":[ { "key":"userWithStatus@ippon.fr", "columns":[ { "name":"fa2bd770-9848-11e1-a6ca-e0f847068d52", "value":"" }, { "name":"f97d6470-9847-11e1-a6ca-e0f847068d52", "value":"" } ] } ] }, { "name":"Domainline", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type" }, { "name":"Status", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "rows":[ { "key":"fa2bd770-9848-11e1-a6ca-e0f847068d52", "columns":[ { "name":"login", "value":"utf8(userWithStatus@ippon.fr)" }, { "name":"username", "value":"utf8(userWithStatus)" }, { "name":"domain", "value":"utf8(ippon.fr)" }, { "name":"content", "value":"utf8(Tatami is an enterprise social network)" }, { "name":"statusDate", "value":"utf8(10/03/2012)" } ] }, { "key":"f97d6470-9847-11e1-a6ca-e0f847068d52", "columns":[ { "name":"login", "value":"utf8(userWithStatus@ippon.fr)" }, { "name":"username", "value":"utf8(userWithStatus)" }, { "name":"domain", "value":"utf8(ippon.fr)" }, { "name":"content", "value":"utf8(Tatami is fully Open Source)" }, { "name":"statusDate", "value":"utf8(11/03/2012)" } ] } ] }, { "name":"Discussion", "keyType":"BytesType", "comparatorType":"BytesType", "defaultColumnValueType":"BytesType" }, { "name":"Shares", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"UTF8Type", "rows":[ { "key":"f97d6470-9847-11e1-a6ca-dummy1", "columns":[ { "name":"111111111", "value":"utf8(john_doe)" } ] } ] }, { "name":"TagFollowers", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"LongType", "rows":[ { "key":"test-ippon.fr", "columns":[ { "name":"jdubois@ippon.fr", "value":"0" } ] } ] }, { "name":"Group", "keyType":"BytesType", "comparatorType":"BytesType", "defaultColumnValueType":"BytesType" }, { "name":"GroupCounter", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"CounterColumnType" }, { "name":"GroupDetails", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"UTF8Type" }, { "name":"UserGroups", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"UTF8Type" }, { "name":"GroupMembers", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"UTF8Type" }, { "name":"Trends", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type" }, { "name":"UserTrends", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type" }, { "name":"TagCounter", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"CounterColumnType" }, { "name":"Mentionline", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type" }, { "name":"Groupline", "keyType":"UTF8Type", "comparatorType":"UUIDType", "defaultColumnValueType":"UTF8Type" }, { "name":"Rss", "keyType":"BytesType", "comparatorType":"BytesType", "defaultColumnValueType":"BytesType" }, { "name":"Registration", "keyType":"UTF8Type", "comparatorType":"BytesType", "defaultColumnValueType":"BytesType" }, { "name":"UserTags", "keyType":"UTF8Type", "comparatorType":"UTF8Type" }, { "name":"UsersBlocked", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"LongType" }, { "name":"ReportedStatus", "keyType":"UTF8Type", "comparatorType":"UTF8Type", "defaultColumnValueType":"UTF8Type" } ] } ================================================ FILE: services/src/test/resources/logback.xml ================================================ utf-8 [%p] %c - %m%n ================================================ FILE: services/src/test/resources/tatami/tatami-test.properties ================================================ # tatami.version can (should) be used to version static resources and thus enable a long cache value. tatami.version=1.0-SNAPSHOT tatami.url=https://tatami.ippon.fr #User configuration tatami.admin.users=tatami@ippon.fr #E-mail configuration smtp.host=mail.ippon.fr smtp.port=25 smtp.user= smtp.password= smtp.from=tatami@ippon.fr #Cassandra configuration cassandra.host=localhost:9171 cassandra.cluster=Tatami cluster cassandra.keyspace=tatami #Elastic Search configuration elasticsearch.engine.mode=embedded elasticsearch.indexNamePrefix=tatami elasticsearch.cluster.name=tatamiCluster elasticsearch.cluster.nodes=127.0.0.1 elasticsearch.cluster.default.communication.port=9300 ================================================ FILE: src/integration/java/fr/ippon/tatami/test/support/LdapTestServer.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package fr.ippon.tatami.test.support; import java.io.BufferedReader; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import org.apache.commons.io.FileUtils; import org.apache.directory.server.core.DefaultDirectoryService; import org.apache.directory.server.core.DirectoryService; import org.apache.directory.server.core.entry.ServerEntry; import org.apache.directory.server.core.partition.Partition; import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition; import org.apache.directory.server.ldap.LdapServer; import org.apache.directory.server.protocol.shared.transport.TcpTransport; import org.apache.directory.shared.ldap.entry.Entry; import org.apache.directory.shared.ldap.entry.EntryAttribute; import org.apache.directory.shared.ldap.entry.Modification; import org.apache.directory.shared.ldap.entry.ModificationOperation; import org.apache.directory.shared.ldap.entry.Value; import org.apache.directory.shared.ldap.entry.client.ClientModification; import org.apache.directory.shared.ldap.entry.client.DefaultClientAttribute; import org.apache.directory.shared.ldap.exception.LdapNameNotFoundException; import org.apache.directory.shared.ldap.ldif.LdifEntry; import org.apache.directory.shared.ldap.ldif.LdifReader; import org.apache.directory.shared.ldap.name.LdapDN; import org.elasticsearch.common.collect.Lists; /** * An embedded ldap test server Based on * http://directory.apache.org/apacheds/1.5/41-embedding-apacheds-into-an-application.html */ public class LdapTestServer { /** * The directory service */ private DirectoryService service; public DirectoryService getService() { return service; } /** * The LDAP server */ private LdapServer server; private static File workingDir = new File("target/ldapServer"); private Partition addPartition(String partitionId, String partitionDn) throws Exception { // Create a new partition named 'ippon'. Partition partition = new JdbmPartition(); partition.setId(partitionId); partition.setSuffix(partitionDn); service.addPartition(partition); return partition; } public void start() throws Exception { // Initialize the LDAP service service = new DefaultDirectoryService(); service.setWorkingDirectory(workingDir); // Disable the ChangeLog system service.getChangeLog().setEnabled(false); service.setDenormalizeOpAttrsEnabled(true); Partition ipponPartition = addPartition("ippon", "dc=ippon,dc=fr"); // And start the service service.startup(); // Inject the ippon root entry if it does not already exist try { service.getAdminSession().lookup(ipponPartition.getSuffixDn()); System.out.printf("Root %s found ! %n", ipponPartition.getSuffixDn()); } catch (LdapNameNotFoundException lnnfe) { System.out.printf("Root %s not found ! creating it ... %n", ipponPartition.getSuffixDn()); LdapDN dnippon = new LdapDN("dc=ippon,dc=fr"); ServerEntry entryippon = service.newEntry(dnippon); entryippon.add("objectClass", "top", "domain", "extensibleObject"); entryippon.add("dc", "ippon"); service.getAdminSession().add(entryippon); System.out.printf("Importing some data ... %n", ipponPartition.getSuffixDn()); InputStream is = this.getClass().getResource("ipponTestLdapExport.ldif").openStream(); LdifReader ldifReader = new LdifReader(is); for (LdifEntry entry : ldifReader) { injectEntry(entry, service); } is.close(); } // service LDAP : server = new LdapServer(); // int serverPort = 10389; int serverPort = 389; server.setTransports(new TcpTransport(serverPort)); server.setDirectoryService(service); server.start(); } public void replaceAttribute(String dn, String attName, String value) throws Exception { LdapDN ldapDN = new LdapDN(dn); EntryAttribute attribute = new DefaultClientAttribute(attName, value); Modification m = new ClientModification(ModificationOperation.REPLACE_ATTRIBUTE, attribute); List l = Lists.newArrayList(m); service.getAdminSession().modify(ldapDN, l); } private static void injectEntry(LdifEntry entry, DirectoryService service) throws Exception { if (entry.isChangeAdd()) { ServerEntry serverEntry = service.newEntry(entry.getDn()); for (EntryAttribute entryAttribute : entry.getEntry()) { List> allValue = new ArrayList>(); for (Value value : entryAttribute) { allValue.add(value); } serverEntry.add(entryAttribute.getId(), allValue.toArray(new Value[0])); } service.getAdminSession().add(serverEntry); // service.getAdminSession().add( new DefaultServerEntry( service.getSchemaManager(), entry.getEntry() ) ); } else if (entry.isChangeModify()) { // not used, not tested ... service.getAdminSession().modify(entry.getDn(), entry.getModificationItems()); } else { throw new IllegalArgumentException("bug"); } } public void stop() throws Exception { server.stop(); service.shutdown(); } /** * Creates a new instance of EmbeddedADS. It initializes the directory service. * * @throws Exception If something went wrong */ public LdapTestServer() throws Exception { } /** * Main class. We just do a lookup on the server to check that it's available. *

    * FIXME : in Eclipse : when running this classes as "Java Application", target/test-classes is not added in the classpath * resulting in a java.lang.ClassNotFoundException ... * * @param args Not used. * @throws Exception */ public static void main(String[] args) throws Exception { FileUtils.deleteDirectory(workingDir); LdapTestServer ads = null; try { // Create the server ads = new LdapTestServer(); ads.start(); // Read an entry Entry result = ads.service.getAdminSession().lookup(new LdapDN("dc=ippon,dc=fr")); // And print it if available System.out.println("Found entry : " + result); } catch (Exception e) { // Ok, we have something wrong going on ... e.printStackTrace(); } System.out.println("Press enter"); new BufferedReader(new InputStreamReader(System.in)).readLine(); ads.stop(); } } ================================================ FILE: src/integration/java/fr/ippon/tatami/test/support/LdapTestServerJunitLauncher.java ================================================ package fr.ippon.tatami.test.support; import org.junit.Test; import static org.junit.Assert.*; /** * In Eclipse : when running LdapTestServer as "Java Application", target/test-classes is not added in the classpath * resulting in a java.lang.ClassNotFoundException ... * This is a workaround ... *

    * Note : this class name does NOT end with *Test (not to be executed by surefire maven plugin) */ public class LdapTestServerJunitLauncher { @Test public void test() throws Exception { LdapTestServer.main(null); } } ================================================ FILE: src/integration/java/fr/ippon/tatami/uitest/AuthenticationSpec.groovy ================================================ package fr.ippon.tatami.uitest import pages.* import pages.google.* import spock.lang.Shared import fr.ippon.tatami.test.support.LdapTestServer import fr.ippon.tatami.uitest.support.TatamiBaseGebSpec class AuthenticationSpec extends TatamiBaseGebSpec { @Shared def newUid static LdapTestServer ldapTestServer def setupSpec() { // It only works if Tatami server is on the same host as the test ... // AND tatami server points to localhost to reach the ldap server ! // TODO : fix tatami configuration ! // TODO : put this into maven ldapTestServer = new LdapTestServer(); ldapTestServer.start(); newUid = "john_doe_${new Date().time}".toString() ldapTestServer.replaceAttribute("cn=john_doe,dc=ippon,dc=fr", "uid", newUid) println "ldap entry patched" } def cleanupSpec() { ldapTestServer.stop(); ldapTestServer = null; } def "login as admin with ldap"() { given: to LoginPage verifyAt() when: loginForm.with { j_username = "jdubois@ippon.fr" j_password = "ippon" } and: loginButton.click() then: waitFor { at HomePage } and: adminLink.isPresent(); } // auto-registration with ldap : def "login as normal new user with ldap"() { given: to LoginPage verifyAt() when: def username = "$newUid@ippon.fr" loginForm.with { // j_username = "john_doe@ippon.fr" j_username = username j_password = "john" } and: loginButton.click() then: waitFor { at HomePage } newUserWizard.isPresent() // TODO : fill wizards ! adminLink.isPresent() if(realBrowser()) { // doesn't work with htmlDriver (when javascript is disabled at least) : assert updateStatus !=null } } // TODO : should we explicitly test that new openid user can login (auto-registration) ? def "login as normal existing user with google"() { given: def googleEmail = System.getProperty("google.email") def googlePassword = System.getProperty("google.password") assert googleEmail !=null assert googlePassword !=null to LoginPage verifyAt() // and google account not already accepting localhost ... when: "click on goodle authentication button" googleButton.click() then : waitFor { at GoogleAuthenticationPage } when : "enter credentials on google" loginForm.with { Email = googleEmail Passwd = googlePassword } loginButton.click() then: waitFor { at GoogleOpenIdPage } when: "Authorize localhost to receive openid authentication on google" rememberChoicesCB.value(false) approveButton.click() then: waitFor { at HomePage } // Note : If it's a new user, the "new user wizard" will also shows up ... ! adminLink.isPresent() } } ================================================ FILE: src/integration/java/fr/ippon/tatami/uitest/NewRegistrationSpec.groovy ================================================ package fr.ippon.tatami.uitest import pages.* import fr.ippon.tatami.uitest.support.TatamiBaseGebSpec class NewRegistrationSpec extends TatamiBaseGebSpec { def "existing user can't register"() { given: to LoginPage verifyAt() def existingUserEmail = "john_doe@ippon.fr" // accountUtils.assertUserExists(existingUserEmail); accountUtils.createUserIfNecessary(existingUserEmail); // TODO : use org.cassandraunit.DataLoader instead to create user before the test ? when: registrationForm.email = existingUserEmail registrationButton.click() then: waitFor { at LoginPage } errorAlert.isPresent() } def "register with new user email"() { given: to LoginPage verifyAt() def newUserEmail = "new_user_${new Date().time}@domain.fr" when: "Filling register form" registrationForm.email = newUserEmail and: "submit register form" registrationButton.click() then: "I'm at LoginPage with msg 'registration sent'" waitFor { at LoginPage } ! errorAlert.isPresent() infoAlert.isPresent() when: "Clicking on registration link (normally sent by email)" def registrationKey = registrationUtils.getRegistrationKeyByLogin(newUserEmail) // go "tatami/register?key=$registrationKey" to([key:registrationKey], EmailVerifiedPage) verifyAt() then: "New user exist in DB" accountUtils.assertUserExists(newUserEmail); // TODO : just testing for existence of user in DB is ok or should I try to log ? how ? password is encrypted in database // when: "Login with newly generated password" // // to LoginPage // verifyAt() // password = "" // read or set in database ... // loginForm.with { // j_username = newUserEmail // j_password = password // } // loginButton.click() // // then: // waitFor { at HomePage } // } } ================================================ FILE: src/integration/java/fr/ippon/tatami/uitest/support/AccountUtils.groovy ================================================ package fr.ippon.tatami.uitest.support; import Constants; import User; import CassandraCounterRepository; import CassandraUserRepository; @Singleton public class AccountUtils { CassandraUserRepository getUserRepository() { def keyspaceOperator = CassandraAccessUtils.instance.keyspaceOperator def em = CassandraAccessUtils.instance.entityManager def counterRepository = new CassandraCounterRepository(keyspaceOperator:keyspaceOperator) def repository = new CassandraUserRepository(keyspaceOperator:keyspaceOperator,em:em,counterRepository:counterRepository) return repository } boolean userExists(String login) { def userExists = getUserRepository().findUserByLogin(login) != null return userExists; } void assertUserExists(String login) { assert userExists(login) } void createUserIfNecessary(String login) { if(! userExists(login)) { User user = new User() user.setLogin(login); user.setTheme(Constants.DEFAULT_THEME); getUserRepository().createUser(user) } } } ================================================ FILE: src/integration/java/fr/ippon/tatami/uitest/support/CassandraAccessUtils.groovy ================================================ package fr.ippon.tatami.uitest.support; import static ColumnFamilyKeys.REGISTRATION_CF; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.beans.ColumnSlice; import me.prettyprint.hector.api.beans.HColumn; import me.prettyprint.hector.api.factory.HFactory; import me.prettyprint.hector.api.query.ColumnQuery; import me.prettyprint.hom.EntityManagerImpl; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.StandardEnvironment; import CassandraConfiguration; @Singleton(lazy=true) public class CassandraAccessUtils { Keyspace keyspaceOperator EntityManagerImpl entityManager CassandraAccessUtils() { // TODO : use tatami.properties or tatami-uitest.properties ? StandardEnvironment env = new StandardEnvironment() def properties = [ "cassandra.host" : "127.0.0.1:9160", "cassandra.clusterName" : "Tatami cluster", "cassandra.keyspace" : "tatami" ] def propSource = new MapPropertySource("testProps",properties); env.getPropertySources().addFirst(propSource) CassandraConfiguration configuration = new CassandraConfiguration(); configuration.env = env keyspaceOperator = configuration.keyspaceOperator() entityManager = configuration.entityManager(keyspaceOperator) } } ================================================ FILE: src/integration/java/fr/ippon/tatami/uitest/support/RegistrationUtils.groovy ================================================ package fr.ippon.tatami.uitest.support; import CassandraRegistrationRepository; @Singleton public class RegistrationUtils { String getRegistrationKeyByLogin(String login) { def keyspaceOperator = CassandraAccessUtils.getInstance().getKeyspaceOperator() def repository = new CassandraRegistrationRepository(keyspaceOperator:keyspaceOperator) def registrationsByLogin = repository._getAllRegistrationKeyByLogin() return registrationsByLogin[login] } } ================================================ FILE: src/integration/java/fr/ippon/tatami/uitest/support/TatamiBaseGebSpec.groovy ================================================ package fr.ippon.tatami.uitest.support; import org.openqa.selenium.htmlunit.HtmlUnitDriver; import geb.spock.GebSpec; public abstract class TatamiBaseGebSpec extends GebSpec { static { // // TODO : il doit y avoir un moyen plus simple/propre de choisir le driver : (cf GebConfig aussi) // // by default we use HtmlUnit (cf GebConfig.groovy ) // // FIXME : delete this : // if(!System.getProperty('gev.env')) { // System.setProperty('geb.env',"chrome") //// System.setProperty('geb.env',"firefox") // } } boolean realBrowser() { ! (getBrowser().getDriver() instanceof HtmlUnitDriver) } RegistrationUtils getRegistrationUtils() { return RegistrationUtils.getInstance(); } AccountUtils getAccountUtils() { return AccountUtils.getInstance(); } } ================================================ FILE: src/integration/java/pages/EmailVerifiedPage.groovy ================================================ package pages import geb.Page; class EmailVerifiedPage extends Page { static url = "tatami/register" // to be use with a param 'key' ?key=86273322226868972417" static at = { $("p",text:contains("Your password will be e-mailed to you"))} } ================================================ FILE: src/integration/java/pages/HomePage.groovy ================================================ package pages import geb.Page; class HomePage extends TatamiBasePage { static url = "tatami/" static at = { profileContent != null } // sometimes I got an error here ?! // TODO : add " or newUserWizard != null" if necessary static content = { // défini dans la classe mère : // dropDownMenu // adminLink profileContent { $("div#profileContent") } // les contenus ci-dessous ne sont accessibles qu'avec le javascript puisque les div sont chargés vides ... // TODO : tjs vrai ? updateStatus { $("textarea#updateStatusContent") } newUserWizard { $("div#modal-welcome") } } } ================================================ FILE: src/integration/java/pages/LoginPage.groovy ================================================ package pages import geb.Page class LoginPage extends Page { static url = "tatami/login" static at = { $("h1",text:contains("Welcome to Tatami")) } static content = { errorAlert(required:false) { $("div.alert-error",text:contains("e-mail address is already in used")) } infoAlert(required:false) { $("div.alert-info",text:contains("registration e-mail")) } registrationForm { $("#registrationForm") } registrationButton { $("#registrationButton") } loginForm { $("#loginForm") } loginButton { $("#loginButton") } // googleForm { $("#googleForm") } googleButton { $("#proceed_google") } } } ================================================ FILE: src/integration/java/pages/TatamiBasePage.groovy ================================================ package pages import geb.Page; class TatamiBasePage extends Page { static content = { dropDownMenu { $("ul.dropdown-menu") } //

    L’essentiel

    Ippevent – Retour d’expérience Applications Mobiles

    Date : Jeudi 20 décembre 2012

    Heure : 19h00 – 22h00

    Lieu : Ippon Technologies – 90 rue Baudin, Levallois Perret

    Comment venir : Métro Pont de Levallois (l.3) – Transilien Clichy Levallois

    Des questions ? marketing@ippon.fr

    Suivez-nous : @ippontechhttp://blog.ippon.fr/www.ippon-mobile.fr

    ]]> http://blog.ippon.fr/2012/12/03/ippevent-mobilite-applications-mobiles-ouverture-des-inscriptions/feed/ 0 http://blog.ippon.fr/2012/12/03/ippevent-mobilite-applications-mobiles-ouverture-des-inscriptions/ ================================================ FILE: web/gruntfile.js ================================================ module.exports = function(grunt) { require('load-grunt-tasks')(grunt); var jsFiles = [ "/assets/bower_components/angular/angular.min.js", "/assets/bower_components/angular-touch/angular-touch.min.js", "/assets/bower_components/angular-resource/angular-resource.min.js", "/assets/bower_components/angular-sanitize/angular-sanitize.min.js", "/assets/bower_components/angular-ui-router/release/angular-ui-router.min.js", "/assets/bower_components/angular-translate/angular-translate.min.js", "/assets/bower_components/angular-cookies/angular-cookies.min.js", "/assets/bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js", "/assets/vendor/js/marked/marked.min.js", "/assets/bower_components/moment/min/moment.min.js", "/assets/bower_components/moment/locale/fr.js", "/assets/bower_components/angular-moment/angular-moment.min.js", "/assets/bower_components/ngInfiniteScroll/build/ng-infinite-scroll.min.js", "/assets/bower_components/angular-bootstrap/ui-bootstrap.min.js", "/assets/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js", "/assets/bower_components/ment.io/dist/mentio.js", "/assets/bower_components/angular-animate/angular-animate.min.js", "/assets/bower_components/ngtoast/dist/ngToast.min.js", "/assets/bower_components/ng-file-upload/angular-file-upload-shim.min.js", "/assets/bower_components/ng-file-upload/angular-file-upload.min.js", "/assets/bower_components/openlayers/OpenLayers.min.js", "/assets/bower_components/angular-local-storage/dist/angular-local-storage.min.js", "/assets/bower_components/jquery/dist/jquery.min.js", "/assets/vendor/css/bootstrap/js/bootstrap.min.js", "/assets/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js", "/assets/bower_components/angular-bootstrap-tour/dist/angular-bootstrap-tour.js", "/app/TatamiApp.js", "/app/shared/topMenu/TopMenuModule.js", "/app/components/login/LoginModule.js", "/app/components/about/AboutModule.js", "/app/components/home/HomeModule.js", "/app/components/account/AccountModule.js", "/app/components/admin/AdminModule.js", "/app/components/about/license/LicenseController.js", "/app/shared/sidebars/home/HomeSidebarModule.js", "/app/shared/sidebars/home/HomeSidebarController.js", "/app/shared/sidebars/profile/ProfileSidebarModule.js", "/app/shared/sidebars/profile/ProfileSidebarController.js", "/app/components/home/tag/TagHeaderController.js", "/app/components/home/search/SearchHeaderController.js", "/app/components/home/group/GroupHeaderController.js", "/app/components/home/profile/ProfileHeaderController.js", "/app/components/home/status/StatusController.js", "/app/shared/lists/status/withoutContext/StatusListController.js", "/app/shared/lists/user/UserListController.js", "/app/shared/topMenu/post/PostModule.js", "/app/shared/topMenu/post/PostController.js", "/app/shared/footer/FooterModule.js", "/app/shared/footer/FooterController.js", "/app/components/home/welcome/WelcomeController.js", "/app/components/admin/AdminController.js", "/app/components/admin/AdminService.js", "/app/components/account/AccountController.js", "/app/components/account/FormController.js", "/app/components/account/profile/ProfileModule.js", "/app/components/account/profile/ProfileController.js", "/app/components/account/preferences/PreferencesModule.js", "/app/components/account/preferences/PreferencesController.js", "/app/components/account/preferences/PreferencesService.js", "/app/components/account/password/PasswordModule.js", "/app/components/account/password/PasswordController.js", "/app/components/account/password/PasswordService.js", "/app/components/account/files/FilesModule.js", "/app/components/account/files/FilesController.js", "/app/components/account/files/FilesService.js", "/app/components/account/users/UsersModule.js", "/app/components/account/users/UsersController.js", "/app/components/account/groups/GroupsModule.js", "/app/components/account/groups/GroupsController.js", "/app/components/account/groups/manage/GroupsManageController.js", "/app/components/account/groups/creation/GroupsCreateController.js", "/app/components/account/groups/list/GroupListController.js", "/app/components/account/tags/TagsModule.js", "/app/components/account/tags/TagsController.js", "/app/components/account/topPosters/TopPostersModule.js", "/app/components/account/topPosters/TopPostersController.js", "/app/components/login/LoginModule.js", "/app/components/login/manual/ManualLoginController.js", "/app/components/login/recoverPassword/RecoverPasswordController.js", "/app/components/login/register/RegisterController.js", "/app/components/login/google/GoogleLoginController.js", "/app/components/login/email/EmailRegistrationController.js", "/app/components/login/RegistrationService.js", "/app/shared/configs/MarkedConfig.js", "/app/shared/configs/MomentConfig.js", "/app/shared/configs/TranslateConfig.js", "/app/shared/filters/MarkdownFilter.js", "/app/shared/filters/EmoticonFilter.js", "/app/shared/filters/PlaceholderFilter.js", "/app/shared/services/HomeService.js", "/app/shared/services/StatusService.js", "/app/shared/services/ProfileService.js", "/app/shared/services/UserService.js", "/app/shared/services/GroupService.js", "/app/shared/services/TagService.js", "/app/shared/services/GeolocService.js", "/app/shared/services/SearchService.js", "/app/shared/services/TopPostersService.js", "/app/shared/services/UserSession.js", "/app/shared/services/AuthenticationService.js", "/app/shared/topMenu/TopMenuController.js" ] var cssFiles = [ "assets/bower_components/ment.io/ment.io/styles.css", "assets/bower_components/ngtoast/dist/ngToast.min.css", "assets/vendor/css/bootstrap/css/bootstrap.css", "assets/bower_components/bootstrap-tour/build/css/bootstrap-tour.css" ] var prepareMinification = function(prepFiles, prefixPath) { var result = []; for(var file = 0; file < prepFiles.length; ++file) { result.push(prefixPath + prepFiles[file]); } return result; } var prepareTags = function(prepFiles, open, close) { var result = ''; var end = '\n\t'; for(var file = 0; file < prepFiles.length; ++file) { if(file == prepFiles.length-1) { end = ''; } result += open + prepFiles[file] + close + end; } return result; } var filePrefix = 'src/main/webapp/'; grunt.initConfig({ template: { prepareMinIndex: { options: { data: { jsTags: '', cssTags: '' } }, files: { 'src/main/webapp/index.html': ['src/main/webapp/index.html.tpl'] } }, prepareDevIndex: { options: { data: { jsTags: prepareTags(jsFiles, ''), cssTags: prepareTags(cssFiles, '') } }, files: { 'src/main/webapp/index.html': ['src/main/webapp/index.html.tpl'] } } }, clean: [ 'src/main/webapp/TATAMI.CONCAT.js', 'src/main/webapp/css/CSSMIN.css', '**/*.min.html', 'src/main/webapp/index.html'], uglify: { options: { mangle: false }, BuildingTatamiConcat: { files: { 'src/main/webapp/TATAMI.CONCAT.js': prepareMinification(jsFiles, filePrefix) } } }, cssmin: { target: { files: { //tatami.css should not be minified-- it breaks. 'src/main/webapp/css/CSSMIN.css': prepareMinification(cssFiles, filePrefix) } } }, htmlmin: { dist: { options: { removeComments: true, collapseWhitespace: true }, files: {// 'destination': 'source' 'src/main/webapp/app/components/about/tos/ToSView.min.html': 'src/main/webapp/app/components/about/tos/ToSView.html', 'src/main/webapp/app/components/about/license/LicenseView.min.html': 'src/main/webapp/app/components/about/license/LicenseView.html', 'src/main/webapp/app/components/about/presentation/PresentationView.min.html': 'src/main/webapp/app/components/about/presentation/PresentationView.html', 'src/main/webapp/app/components/account/AccountView.min.html': 'src/main/webapp/app/components/account/AccountView.html', 'src/main/webapp/app/components/account/profile/ProfileView.min.html': 'src/main/webapp/app/components/account/profile/ProfileView.html', 'src/main/webapp/app/components/account/preferences/PreferencesView.min.html':'src/main/webapp/app/components/account/preferences/PreferencesView.html', 'src/main/webapp/app/components/account/password/PasswordView.min.html': 'src/main/webapp/app/components/account/password/PasswordView.html', 'src/main/webapp/app/components/account/files/FilesView.min.html':'src/main/webapp/app/components/account/files/FilesView.html', 'src/main/webapp/app/components/account/FormView.min.html':'src/main/webapp/app/components/account/FormView.html', 'src/main/webapp/app/components/account/users/UsersView.min.html':'src/main/webapp/app/components/account/users/UsersView.html', 'src/main/webapp/app/components/account/groups/GroupsView.min.html':'src/main/webapp/app/components/account/groups/GroupsView.html', 'src/main/webapp/app/components/account/groups/creation/GroupsCreateView.min.html':'src/main/webapp/app/components/account/groups/creation/GroupsCreateView.html', 'src/main/webapp/app/components/account/groups/list/GroupsListView.min.html':'src/main/webapp/app/components/account/groups/list/GroupsListView.html', 'src/main/webapp/app/components/account/groups/manage/GroupsManageView.min.html':'src/main/webapp/app/components/account/groups/manage/GroupsManageView.html', 'src/main/webapp/app/components/account/tags/TagsView.min.html':'src/main/webapp/app/components/account/tags/TagsView.html', 'src/main/webapp/app/components/account/topPosters/TopPostersView.min.html':'src/main/webapp/app/components/account/topPosters/TopPostersView.html', 'src/main/webapp/app/components/admin/AdminView.min.html':'src/main/webapp/app/components/admin/AdminView.html', 'src/main/webapp/app/components/home/HomeView.min.html':'src/main/webapp/app/components/home/HomeView.html', 'src/main/webapp/app/components/home/status/StatusView.min.html':'src/main/webapp/app/components/home/status/StatusView.html', 'src/main/webapp/app/components/home/search/SearchHeaderView.min.html':'src/main/webapp/app/components/home/search/SearchHeaderView.html', 'src/main/webapp/app/components/home/tag/TagHeaderView.min.html':'src/main/webapp/app/components/home/tag/TagHeaderView.html', 'src/main/webapp/app/shared/lists/status/withoutContext/StatusListView.min.html':'src/main/webapp/app/shared/lists/status/withoutContext/StatusListView.html', 'src/main/webapp/app/shared/sidebars/home/HomeSidebarView.min.html':'src/main/webapp/app/shared/sidebars/home/HomeSidebarView.html', 'src/main/webapp/app/components/home/timeline/TimelineHeaderView.min.html':'src/main/webapp/app/components/home/timeline/TimelineHeaderView.html', 'src/main/webapp/app/components/home/welcome/WelcomeView.min.html':'src/main/webapp/app/components/home/welcome/WelcomeView.html', 'src/main/webapp/app/shared/lists/user/UserListView.min.html':'src/main/webapp/app/shared/lists/user/UserListView.html', 'src/main/webapp/app/components/home/group/GroupHeaderView.min.html':'src/main/webapp/app/components/home/group/GroupHeaderView.html', 'src/main/webapp/app/shared/topMenu/post/PostView.min.html':'src/main/webapp/app/shared/topMenu/post/PostView.html', 'src/main/webapp/app/shared/sidebars/profile/ProfileSidebarView.min.html':'src/main/webapp/app/shared/sidebars/profile/ProfileSidebarView.html', 'src/main/webapp/app/components/home/profile/ProfileHeaderView.min.html':'src/main/webapp/app/components/home/profile/ProfileHeaderView.html', //Login Module 'src/main/webapp/app/components/login/LoginView.min.html':'src/main/webapp/app/components/login/LoginView.html', 'src/main/webapp/app/components/login/manual/ManualLoginView.min.html':'src/main/webapp/app/components/login/manual/ManualLoginView.html', 'src/main/webapp/app/components/login/recoverPassword/RecoverPasswordView.min.html':'src/main/webapp/app/components/login/recoverPassword/RecoverPasswordView.html', 'src/main/webapp/app/components/login/google/GoogleLoginView.min.html':'src/main/webapp/app/components/login/google/GoogleLoginView.html', 'src/main/webapp/app/components/login/register/RegisterView.min.html':'src/main/webapp/app/components/login/register/RegisterView.html', 'src/main/webapp/app/components/login/email/EmailRegistration.min.html':'src/main/webapp/app/components/login/email/EmailRegistration.html', //TatamiApp module 'src/main/webapp/app/shared/topMenu/TopMenuView.min.html':'src/main/webapp/app/shared/topMenu/TopMenuView.html', 'src/main/webapp/app/shared/footer/FooterView.min.html':'src/main/webapp/app/shared/footer/FooterView.html', 'src/main/webapp/app/shared/error/404View.min.html':'src/main/webapp/app/shared/error/404View.html', 'src/main/webapp/app/shared/error/500View.min.html':'src/main/webapp/app/shared/error/500View.html' } } }, concurrent: { minifyTarget: ['template:prepareMinIndex', 'cssmin', 'htmlmin', 'uglify'], devTarget: ['template:prepareDevIndex', 'htmlmin'] } } ); grunt.loadNpmTasks('grunt-template'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-htmlmin'); grunt.registerTask('minify', ['clean','concurrent:minifyTarget']); grunt.registerTask('dev', ['clean', 'concurrent:devTarget']); }; ================================================ FILE: web/karma.ci.conf.js ================================================ // Karma configuration // Generated on Wed May 06 2015 15:23:11 GMT-0400 (EDT) module.exports = function(config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['jasmine'], // list of files / patterns to load in the browser files: [ "src/main/webapp/assets/bower_components/angular/angular.min.js", "src/main/webapp/assets/bower_components/angular-mocks/angular-mocks.js", "src/main/webapp/assets/bower_components/angular-route/angular-route.min.js", "src/main/webapp/assets/bower_components/angular-touch/angular-touch.min.js", "src/main/webapp/assets/bower_components/angular-resource/angular-resource.min.js", "src/main/webapp/assets/bower_components/angular-sanitize/angular-sanitize.min.js", "src/main/webapp/assets/bower_components/angular-ui-router/release/angular-ui-router.min.js", "src/main/webapp/assets/bower_components/angular-translate/angular-translate.min.js", "src/main/webapp/assets/bower_components/angular-cookies/angular-cookies.min.js", "src/main/webapp/assets/bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js", "src/main/webapp/assets/vendor/js/marked/marked.min.js", "src/main/webapp/assets/bower_components/moment/min/moment.min.js", "src/main/webapp/assets/bower_components/moment/locale/fr.js", "src/main/webapp/assets/bower_components/angular-moment/angular-moment.min.js", "src/main/webapp/assets/bower_components/ngInfiniteScroll/build/ng-infinite-scroll.min.js", "src/main/webapp/assets/bower_components/angular-bootstrap/ui-bootstrap.min.js", "src/main/webapp/assets/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js", "src/main/webapp/assets/bower_components/ment.io/dist/mentio.js", "src/main/webapp/assets/bower_components/angular-animate/angular-animate.min.js", "src/main/webapp/assets/bower_components/ngtoast/dist/ngToast.min.js", "src/main/webapp/assets/bower_components/ng-file-upload/angular-file-upload-shim.min.js", "src/main/webapp/assets/bower_components/ng-file-upload/angular-file-upload.min.js", "src/main/webapp/assets/bower_components/openlayers/OpenLayers.min.js", "src/main/webapp/assets/bower_components/angular-local-storage/dist/angular-local-storage.min.js", "src/main/webapp/assets/bower_components/jquery/dist/jquery.min.js", "src/main/webapp/assets/vendor/css/bootstrap/js/bootstrap.min.js", "src/main/webapp/assets/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js", "src/main/webapp/assets/bower_components/angular-bootstrap-tour/dist/angular-bootstrap-tour.js", "src/main/webapp/app/TatamiApp.js", "src/main/webapp/app/shared/topMenu/TopMenuModule.js", "src/main/webapp/app/components/login/LoginModule.js", "src/main/webapp/app/components/about/AboutModule.js", "src/main/webapp/app/components/home/HomeModule.js", "src/main/webapp/app/components/account/AccountModule.js", "src/main/webapp/app/components/admin/AdminModule.js", "src/main/webapp/app/components/about/license/LicenseController.js", "src/main/webapp/app/shared/sidebars/home/HomeSidebarModule.js", "src/main/webapp/app/shared/sidebars/home/HomeSidebarController.js", "src/main/webapp/app/shared/sidebars/profile/ProfileSidebarModule.js", "src/main/webapp/app/shared/sidebars/profile/ProfileSidebarController.js", "src/main/webapp/app/components/home/tag/TagHeaderController.js", "src/main/webapp/app/components/home/search/SearchHeaderController.js", "src/main/webapp/app/components/home/group/GroupHeaderController.js", "src/main/webapp/app/components/home/profile/ProfileHeaderController.js", "src/main/webapp/app/components/home/status/StatusController.js", "src/main/webapp/app/shared/lists/status/withoutContext/StatusListController.js", "src/main/webapp/app/shared/lists/user/UserListController.js", "src/main/webapp/app/shared/topMenu/post/PostModule.js", "src/main/webapp/app/shared/topMenu/post/PostController.js", "src/main/webapp/app/components/home/welcome/WelcomeController.js", "src/main/webapp/app/components/admin/AdminController.js", "src/main/webapp/app/components/admin/AdminService.js", "src/main/webapp/app/components/account/AccountController.js", "src/main/webapp/app/components/account/FormController.js", "src/main/webapp/app/components/account/profile/ProfileModule.js", "src/main/webapp/app/components/account/profile/ProfileController.js", "src/main/webapp/app/components/account/preferences/PreferencesModule.js", "src/main/webapp/app/components/account/preferences/PreferencesController.js", "src/main/webapp/app/components/account/preferences/PreferencesService.js", "src/main/webapp/app/components/account/password/PasswordModule.js", "src/main/webapp/app/components/account/password/PasswordController.js", "src/main/webapp/app/components/account/password/PasswordService.js", "src/main/webapp/app/components/account/files/FilesModule.js", "src/main/webapp/app/components/account/files/FilesController.js", "src/main/webapp/app/components/account/files/FilesService.js", "src/main/webapp/app/components/account/users/UsersModule.js", "src/main/webapp/app/components/account/users/UsersController.js", "src/main/webapp/app/components/account/groups/GroupsModule.js", "src/main/webapp/app/components/account/groups/GroupsController.js", "src/main/webapp/app/components/account/groups/manage/GroupsManageController.js", "src/main/webapp/app/components/account/groups/creation/GroupsCreateController.js", "src/main/webapp/app/components/account/groups/list/GroupListController.js", "src/main/webapp/app/components/account/tags/TagsModule.js", "src/main/webapp/app/components/account/tags/TagsController.js", "src/main/webapp/app/components/account/topPosters/TopPostersModule.js", "src/main/webapp/app/components/account/topPosters/TopPostersController.js", "src/main/webapp/app/components/login/LoginModule.js", "src/main/webapp/app/components/login/manual/ManualLoginController.js", "src/main/webapp/app/components/login/recoverPassword/RecoverPasswordController.js", "src/main/webapp/app/components/login/register/RegisterController.js", "src/main/webapp/app/components/login/google/GoogleLoginController.js", "src/main/webapp/app/components/login/email/EmailRegistrationController.js", "src/main/webapp/app/components/login/RegistrationService.js", "src/main/webapp/app/shared/configs/MarkedConfig.js", "src/main/webapp/app/shared/configs/MomentConfig.js", "src/main/webapp/app/shared/configs/TranslateConfig.js", "src/main/webapp/app/shared/filters/MarkdownFilter.js", "src/main/webapp/app/shared/filters/EmoticonFilter.js", "src/main/webapp/app/shared/filters/PlaceholderFilter.js", "src/main/webapp/app/shared/services/HomeService.js", "src/main/webapp/app/shared/services/StatusService.js", "src/main/webapp/app/shared/services/ProfileService.js", "src/main/webapp/app/shared/services/UserService.js", "src/main/webapp/app/shared/services/GroupService.js", "src/main/webapp/app/shared/services/TagService.js", "src/main/webapp/app/shared/services/GeolocService.js", "src/main/webapp/app/shared/services/SearchService.js", "src/main/webapp/app/shared/services/TopPostersService.js", "src/main/webapp/app/shared/services/UserSession.js", "src/main/webapp/app/shared/services/AuthenticationService.js", "src/main/webapp/app/shared/topMenu/TopMenuController.js", "src/test/javascript/**/*.js" ], // list of files to exclude exclude: [ '**/*.swp' ], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { 'src/**/*.js': ['coverage'] }, // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ['dots', 'jenkins','coverage', 'progress'], jenkinsReporter: { outputFile: 'target/test-results/karma/TESTS-resuts.xml' }, coverageReporter: { dir: 'target/test-results/coverage', reporters: [ { type: 'lcov', subdir: 'report-lcov'} ] }, // web server port port: 9876, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes autoWatch: false, // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: ['PhantomJS'], // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: true }); }; ================================================ FILE: web/karma.conf.js ================================================ // Karma configuration // Generated on Wed May 06 2015 15:23:11 GMT-0400 (EDT) module.exports = function(config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['jasmine'], // list of files / patterns to load in the browser files: [ "src/main/webapp/assets/bower_components/angular/angular.min.js", "src/main/webapp/assets/bower_components/angular-mocks/angular-mocks.js", "src/main/webapp/assets/bower_components/angular-route/angular-route.min.js", "src/main/webapp/assets/bower_components/angular-touch/angular-touch.min.js", "src/main/webapp/assets/bower_components/angular-resource/angular-resource.min.js", "src/main/webapp/assets/bower_components/angular-sanitize/angular-sanitize.min.js", "src/main/webapp/assets/bower_components/angular-ui-router/release/angular-ui-router.min.js", "src/main/webapp/assets/bower_components/angular-translate/angular-translate.min.js", "src/main/webapp/assets/bower_components/angular-cookies/angular-cookies.min.js", "src/main/webapp/assets/bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.min.js", "src/main/webapp/assets/vendor/js/marked/marked.min.js", "src/main/webapp/assets/bower_components/moment/min/moment.min.js", "src/main/webapp/assets/bower_components/moment/locale/fr.js", "src/main/webapp/assets/bower_components/angular-moment/angular-moment.min.js", "src/main/webapp/assets/bower_components/ngInfiniteScroll/build/ng-infinite-scroll.min.js", "src/main/webapp/assets/bower_components/angular-bootstrap/ui-bootstrap.min.js", "src/main/webapp/assets/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js", "src/main/webapp/assets/bower_components/ment.io/dist/mentio.js", "src/main/webapp/assets/bower_components/angular-animate/angular-animate.min.js", "src/main/webapp/assets/bower_components/ngtoast/dist/ngToast.min.js", "src/main/webapp/assets/bower_components/ng-file-upload/angular-file-upload-shim.min.js", "src/main/webapp/assets/bower_components/ng-file-upload/angular-file-upload.min.js", "src/main/webapp/assets/bower_components/openlayers/OpenLayers.min.js", "src/main/webapp/assets/bower_components/angular-local-storage/dist/angular-local-storage.min.js", "src/main/webapp/assets/bower_components/jquery/dist/jquery.min.js", "src/main/webapp/assets/vendor/css/bootstrap/js/bootstrap.min.js", "src/main/webapp/assets/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js", "src/main/webapp/assets/bower_components/angular-bootstrap-tour/dist/angular-bootstrap-tour.js", "src/main/webapp/app/TatamiApp.js", "src/main/webapp/app/shared/topMenu/TopMenuModule.js", "src/main/webapp/app/components/login/LoginModule.js", "src/main/webapp/app/components/about/AboutModule.js", "src/main/webapp/app/components/home/HomeModule.js", "src/main/webapp/app/components/account/AccountModule.js", "src/main/webapp/app/components/admin/AdminModule.js", "src/main/webapp/app/components/about/license/LicenseController.js", "src/main/webapp/app/shared/sidebars/home/HomeSidebarModule.js", "src/main/webapp/app/shared/sidebars/home/HomeSidebarController.js", "src/main/webapp/app/shared/sidebars/profile/ProfileSidebarModule.js", "src/main/webapp/app/shared/sidebars/profile/ProfileSidebarController.js", "src/main/webapp/app/components/home/tag/TagHeaderController.js", "src/main/webapp/app/components/home/search/SearchHeaderController.js", "src/main/webapp/app/components/home/group/GroupHeaderController.js", "src/main/webapp/app/components/home/profile/ProfileHeaderController.js", "src/main/webapp/app/components/home/status/StatusController.js", "src/main/webapp/app/shared/lists/status/withoutContext/StatusListController.js", "src/main/webapp/app/shared/lists/user/UserListController.js", "src/main/webapp/app/shared/topMenu/post/PostModule.js", "src/main/webapp/app/shared/topMenu/post/PostController.js", "src/main/webapp/app/components/home/welcome/WelcomeController.js", "src/main/webapp/app/components/admin/AdminController.js", "src/main/webapp/app/components/admin/AdminService.js", "src/main/webapp/app/components/account/AccountController.js", "src/main/webapp/app/components/account/FormController.js", "src/main/webapp/app/components/account/profile/ProfileModule.js", "src/main/webapp/app/components/account/profile/ProfileController.js", "src/main/webapp/app/components/account/preferences/PreferencesModule.js", "src/main/webapp/app/components/account/preferences/PreferencesController.js", "src/main/webapp/app/components/account/preferences/PreferencesService.js", "src/main/webapp/app/components/account/password/PasswordModule.js", "src/main/webapp/app/components/account/password/PasswordController.js", "src/main/webapp/app/components/account/password/PasswordService.js", "src/main/webapp/app/components/account/files/FilesModule.js", "src/main/webapp/app/components/account/files/FilesController.js", "src/main/webapp/app/components/account/files/FilesService.js", "src/main/webapp/app/components/account/users/UsersModule.js", "src/main/webapp/app/components/account/users/UsersController.js", "src/main/webapp/app/components/account/groups/GroupsModule.js", "src/main/webapp/app/components/account/groups/GroupsController.js", "src/main/webapp/app/components/account/groups/manage/GroupsManageController.js", "src/main/webapp/app/components/account/groups/creation/GroupsCreateController.js", "src/main/webapp/app/components/account/groups/list/GroupListController.js", "src/main/webapp/app/components/account/tags/TagsModule.js", "src/main/webapp/app/components/account/tags/TagsController.js", "src/main/webapp/app/components/account/topPosters/TopPostersModule.js", "src/main/webapp/app/components/account/topPosters/TopPostersController.js", "src/main/webapp/app/components/login/LoginModule.js", "src/main/webapp/app/components/login/manual/ManualLoginController.js", "src/main/webapp/app/components/login/recoverPassword/RecoverPasswordController.js", "src/main/webapp/app/components/login/register/RegisterController.js", "src/main/webapp/app/components/login/google/GoogleLoginController.js", "src/main/webapp/app/components/login/email/EmailRegistrationController.js", "src/main/webapp/app/components/login/RegistrationService.js", "src/main/webapp/app/shared/configs/MarkedConfig.js", "src/main/webapp/app/shared/configs/MomentConfig.js", "src/main/webapp/app/shared/configs/TranslateConfig.js", "src/main/webapp/app/shared/filters/MarkdownFilter.js", "src/main/webapp/app/shared/filters/EmoticonFilter.js", "src/main/webapp/app/shared/filters/PlaceholderFilter.js", "src/main/webapp/app/shared/services/HomeService.js", "src/main/webapp/app/shared/services/StatusService.js", "src/main/webapp/app/shared/services/ProfileService.js", "src/main/webapp/app/shared/services/UserService.js", "src/main/webapp/app/shared/services/GroupService.js", "src/main/webapp/app/shared/services/TagService.js", "src/main/webapp/app/shared/services/GeolocService.js", "src/main/webapp/app/shared/services/SearchService.js", "src/main/webapp/app/shared/services/TopPostersService.js", "src/main/webapp/app/shared/services/UserSession.js", "src/main/webapp/app/shared/services/AuthenticationService.js", "src/main/webapp/app/shared/topMenu/TopMenuController.js", "src/test/javascript/**/*.js" ], // list of files to exclude exclude: [ '**/*.swp' ], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { }, // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ['dots', 'progress' ], htmlReporter: { outputFile: 'target/test-results/karma/results.html' }, // web server port port: 9876, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes autoWatch: true, // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: ['PhantomJS'], // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: false }); }; ================================================ FILE: web/package.json ================================================ { "name": "Tatami", "description": "", "version": "4.0.1", "private": true, "devDependencies": { "bower": "^1.5.3", "grunt": "^0.4.5", "grunt-cli": "^0.1.13", "grunt-concat": "^0.1.6", "grunt-concurrent": "^1.0.0", "grunt-contrib-clean": "^0.6.0", "grunt-contrib-cssmin": "^0.12.2", "grunt-contrib-htmlmin": "^0.4.0", "grunt-contrib-uglify": "^0.9.1", "grunt-template": "^0.2.3", "jasmine-core": "^2.3.4", "karma": "^0.13.10", "karma-chrome-launcher": "^0.2.0", "karma-cli": "^0.1.1", "karma-coverage": "^0.5.2", "karma-firefox-launcher": "^0.1.6", "karma-ie-launcher": "^0.2.0", "karma-jasmine": "^0.3.6", "karma-jenkins-reporter": "0.0.2", "karma-jshint": "^0.1.0", "karma-junit-reporter": "^0.3.6", "karma-phantomjs-launcher": "^0.2.1", "karma-safari-launcher": "^0.1.1", "load-grunt-tasks": "^3.3.0", "phantomjs": "^1.9.18" } } ================================================ FILE: web/pom.xml ================================================ tatami fr.ippon.tatami 4.0.5 4.0.0 war web fr.ippon.tatami services 4.0.5 fr.ippon.tatami services 4.0.5 test-jar test default true org.codehaus.mojo exec-maven-plugin 1.2.1 compile exec ${project.npm.bin}/grunt dev uglified org.codehaus.mojo exec-maven-plugin 1.2.1 compile exec ${project.npm.bin}/grunt minify tatami-${project.version} com.kelveden maven-karma-plugin 1.6 test start ${project.basedir}/node_modules/.bin/karma ${project.basedir}/karma.ci.conf.js org.apache.maven.plugins maven-war-plugin ${maven.war.version} true ${project.version} ${buildNumber} ${maven.build.timestamp} src/main/webapp/WEB-INF WEB-INF web.xml true maven-clean-plugin 2.6.1 ${project.basedir}/node_modules/ false org.codehaus.mojo exec-maven-plugin 1.2.1 install-npm-dependencies generate-sources npm install exec org.apache.maven.plugins maven-remote-resources-plugin 1.5 process fr.ippon.tatami:services:${project.version} ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/atmosphere/RealtimeService.java ================================================ package fr.ippon.tatami.web.atmosphere; import com.fasterxml.jackson.databind.ObjectMapper; import org.atmosphere.config.service.ManagedService; import org.atmosphere.cpr.AtmosphereResource; import org.atmosphere.cpr.AtmosphereResponse; import org.atmosphere.cpr.Broadcaster; import org.atmosphere.cpr.BroadcasterFactory; import org.atmosphere.handler.OnMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; @ManagedService( path = "/realtime/statuses/home_timeline") public class RealtimeService extends OnMessage { private static final Logger log = LoggerFactory.getLogger(RealtimeService.class); private static final ObjectMapper jsonObjectMapper = new ObjectMapper(); @Override public void onOpen(AtmosphereResource resource) throws IOException { log.debug("Opening Atmosphere connection"); String broadcasterName = "/realtime/statuses/home_timeline/" + resource.getRequest().getRemoteUser(); log.debug("Subscribing this resource to broadcaster: {}", broadcasterName); Broadcaster b = BroadcasterFactory.getDefault().lookup(broadcasterName, true); b.addAtmosphereResource(resource); } @Override public void onResume(AtmosphereResponse response) throws IOException { log.debug("Resuming Atmosphere connection"); } @Override public void onTimeout(AtmosphereResponse response) throws IOException { log.debug("Atmosphere connection timeout"); } @Override public void onDisconnect(AtmosphereResponse response) throws IOException { log.debug("Closing Atmosphere connection"); } @Override public void onMessage(AtmosphereResponse response, TatamiNotification notification) throws IOException { log.debug("Received Atmosphere message: {}", notification); String json = jsonObjectMapper.writeValueAsString(notification); response.getWriter().write(json); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/controller/ErrorController.java ================================================ package fr.ippon.tatami.web.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; /** * @author Julien Dubois */ @Controller public class ErrorController { private final Logger log = LoggerFactory.getLogger(ErrorController.class); @RequestMapping(value = "/errors/404") public String pageNotFound(HttpServletRequest request) { log.debug("404 error : {}", request.getAttribute("javax.servlet.forward.request_uri")); return "errors/404"; } @RequestMapping(value = "/errors/500") public String internalServerError(HttpServletRequest request) { log.debug("500 error !"); return "errors/500"; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/controller/HomeController.java ================================================ package fr.ippon.tatami.web.controller; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.security.crypto.password.StandardPasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import javax.inject.Inject; /** * @author Julien Dubois */ @Controller public class HomeController { private final Logger log = LoggerFactory.getLogger(HomeController.class); @Inject private UserService userService; @Inject private AuthenticationService authenticationService; @Inject private Environment env; @RequestMapping(value = "/login", method = RequestMethod.GET) public ModelAndView login(@RequestParam(required = false) String action) { ModelAndView mv = new ModelAndView("login"); mv.addObject("action", action); return mv; } @RequestMapping(value = {"/", "/home/**", "/home", "/home/"}, method = RequestMethod.GET) public ModelAndView home(@RequestParam(required = false) String ios) { ModelAndView mv = new ModelAndView("home"); User currentUser = authenticationService.getCurrentUser(); mv.addObject("user", currentUser); if (ios == null) { mv.addObject("ios", false); } else { mv.addObject("ios", true); } return mv; } @RequestMapping(value = "/register", method = RequestMethod.POST) public String register(@RequestParam String email) { email = email.toLowerCase(); if (userService.getUserByLogin(email) != null) { return "redirect:/tatami/login?action=registerFailure"; } User user = new User(); user.setLogin(email); userService.registerUser(user); return "redirect:/tatami/login?action=register"; } @RequestMapping(value = "/register", method = RequestMethod.GET) public ModelAndView validateRegistration(@RequestParam String key) { ModelAndView mv = new ModelAndView("register"); String login = userService.validateRegistration(key); mv.addObject("login", login); return mv; } @RequestMapping(value = "/register/automatic", method = RequestMethod.POST) public String automaticRegistration(@RequestParam String email, @RequestParam String password) { String enabled = env.getProperty("tatami.automatic.registration"); if (enabled != null && !enabled.equals("true")) { log.warn("Automatic registration should not have been called."); return "redirect:/tatami/login"; } if (email == null || email.equals("")) { return "redirect:/tatami/login"; } email = email.toLowerCase(); if (userService.getUserByLogin(email) != null) { log.debug("User {} already exists.", email); return "redirect:/tatami/login"; } if (email.equals(Constants.TATAMIBOT_NAME)) { log.debug("E-mail {} can only be used by the Tatami Bot.", email); return "redirect:/tatami/login"; } log.debug("Creating user {}", email); User user = new User(); user.setLogin(email); StandardPasswordEncoder encoder = new StandardPasswordEncoder(); String encryptedPassword = encoder.encode(password); user.setPassword(encryptedPassword); userService.createUser(user); return "redirect:/tatami/login"; } @RequestMapping(value = "/lostpassword", method = RequestMethod.POST) public String lostPassword(@RequestParam String email) { email = email.toLowerCase(); User user = userService.getUserByLogin(email); if (user == null) { return "redirect:/tatami/login?action=lostPasswordFailure"; } if (userService.isDomainHandledByLDAP(user.getDomain())) { return "redirect:/tatami/login?action=ldapPasswordFailure"; } userService.lostPassword(user); return "redirect:/tatami/login?action=lostPassword"; } @RequestMapping(value = "/tos", method = RequestMethod.GET) public String termsOfService() { return "terms_of_service"; } @RequestMapping(value = "/presentation", method = RequestMethod.GET) public String presentation() { return "presentation"; } @RequestMapping(value = "/license", method = RequestMethod.GET) public String license() { return "license"; } /** * This maps any GET request to /tatami/customization/[subpath] * to the jsp named /customization/[subpath].jsp. *

    * It allows adding easily new pages with tatamiCustomization */ @RequestMapping(value = "/customization/{subPath}", method = RequestMethod.GET) public String anyOtherSubPath(@PathVariable String subPath) { return "/customization/" + subPath; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/controller/Pac4JSecurityCheckController.java ================================================ package fr.ippon.tatami.web.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class Pac4JSecurityCheckController { @RequestMapping(value = "/j_spring_pac4j_security_check") public String googleCheck() { return "redirect:/#/home"; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/fileupload/FileController.java ================================================ package fr.ippon.tatami.web.fileupload; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.Attachment; import fr.ippon.tatami.domain.Avatar; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.UserRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.AttachmentService; import fr.ippon.tatami.service.AvatarService; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.exception.StorageSizeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.ModelAndView; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Date; import java.util.List; @Controller public class FileController { private static final Logger log = LoggerFactory.getLogger(FileController.class); private static final String HEADER_EXPIRES = "Expires"; private static final String HEADER_CACHE_CONTROL = "Cache-Control"; private static final int CACHE_SECONDS = 60 * 60 * 24 * 30; private static final String HEADER_ETAG = "ETag"; private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; private String tatamiUrl; @Inject private Environment env; @Inject private AttachmentService attachmentService; @Inject private AvatarService avatarService; @Inject private UserService userService; @Inject private UserRepository userRepository; @Inject private AuthenticationService authenticationService; @PostConstruct public void init() { this.tatamiUrl = env.getProperty("tatami.url"); } @RequestMapping(value = "/file/{attachmentId}/*", method = RequestMethod.GET) @Timed public void download(@PathVariable("attachmentId") String attachmentId, HttpServletRequest request, HttpServletResponse response) throws IOException { // Cache the file in the browser response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + CACHE_SECONDS * 1000L); response.setHeader(HEADER_CACHE_CONTROL, "max-age=" + CACHE_SECONDS + ", must-revalidate"); // Put the file in the response Attachment attachment = attachmentService.getAttachmentById(attachmentId); if (attachment == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); response.sendRedirect("/tatami/file/file_not_found"); } else { // ETag support response.setHeader(HEADER_ETAG, attachmentId); // The attachmentId is unique and should not be modified String requestETag = request.getHeader(HEADER_IF_NONE_MATCH); if (requestETag != null && requestETag.equals(attachmentId)) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } else { try { byte[] fileContent = attachment.getContent(); response.getOutputStream().write(fileContent); } catch (IOException e) { log.info("Error writing file to output stream. {}", e.getMessage()); } } } try { response.flushBuffer(); } catch (IOException e) { log.info("Error flushing the output stream. {}", e.getMessage()); } } @RequestMapping(value = "/thumbnail/{attachmentId}/*", method = RequestMethod.GET) @Timed public void thumbnail(@PathVariable("attachmentId") String attachmentId, HttpServletRequest request, HttpServletResponse response) throws IOException { // Cache the file in the browser response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + CACHE_SECONDS * 1000L); response.setHeader(HEADER_CACHE_CONTROL, "max-age=" + CACHE_SECONDS + ", must-revalidate"); // Put the file in the response Attachment attachment = attachmentService.getAttachmentById(attachmentId); if (attachment == null || attachment.getThumbnail().length == 0) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); response.sendRedirect("/tatami/file/file_not_found"); } else { // ETag support response.setHeader(HEADER_ETAG, attachmentId); // The attachmentId is unique and should not be modified String requestETag = request.getHeader(HEADER_IF_NONE_MATCH); if (requestETag != null && requestETag.equals(attachmentId)) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } else { try { byte[] fileContent = attachment.getThumbnail(); response.getOutputStream().write(fileContent); } catch (IOException e) { log.info("Error writing file to output stream. {}", e.getMessage()); } } } try { response.flushBuffer(); } catch (IOException e) { log.info("Error flushing the output stream. {}", e.getMessage()); } } @RequestMapping(value = "/avatar/{avatarId}/*", method = RequestMethod.GET) @Timed public void getAvatar(@PathVariable("avatarId") String avatarId, HttpServletRequest request, HttpServletResponse response) throws IOException { // Cache the file in the browser response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + CACHE_SECONDS * 1000L); response.setHeader(HEADER_CACHE_CONTROL, "max-age=" + CACHE_SECONDS + ", must-revalidate"); // Put the file in the response Avatar avatar = avatarService.getAvatarById(avatarId); if (avatarId == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } else { // ETag support response.setHeader(HEADER_ETAG, avatarId); // The attachmentId is unique and should not be modified String requestETag = request.getHeader(HEADER_IF_NONE_MATCH); if (requestETag != null && requestETag.equals(avatarId)) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } else { try { byte[] fileContent = avatar.getContent(); response.getOutputStream().write(fileContent); } catch (IOException e) { log.info("Error writing file to output stream. {}", e.getMessage()); } } } try { response.flushBuffer(); } catch (IOException e) { log.info("Error flushing the output stream. {}", e.getMessage()); } } @RequestMapping(value = "/rest/fileupload/avatar", method = RequestMethod.POST) @ResponseBody @Timed public List uploadAvatar( @RequestParam("uploadFile") MultipartFile file) throws IOException { Avatar avatar = new Avatar(); avatar.setContent(file.getBytes()); avatar.setFilename(file.getOriginalFilename()); avatar.setSize(file.getSize()); avatar.setCreationDate(new Date()); avatarService.createAvatar(avatar); List uploadedFiles = new ArrayList(); UploadedFile uploadedFile = new UploadedFile( avatar.getAvatarId(), file.getOriginalFilename(), Long.valueOf(file.getSize()).intValue(), tatamiUrl + "/tatami/avatar/" + avatar.getAvatarId() + "/" + file.getOriginalFilename()); log.info("Avatar url : {}/tatami/avatar/{}/{}", tatamiUrl, avatar.getAvatarId(), file.getOriginalFilename()); uploadedFiles.add(uploadedFile); User user = authenticationService.getCurrentUser(); user.setAvatar(avatar.getAvatarId()); userRepository.updateUser(user); return uploadedFiles; } @RequestMapping(value = "/rest/fileupload/avatarIE", headers = "content-type=multipart/*", method = RequestMethod.POST) @ResponseBody @Timed public void uploadAvatarIE( @RequestParam("uploadFile") MultipartFile file) throws IOException { Avatar avatar = new Avatar(); avatar.setContent(file.getBytes()); avatar.setFilename(file.getOriginalFilename()); avatar.setSize(file.getSize()); avatar.setCreationDate(new Date()); avatarService.createAvatar(avatar); log.info("Avatar url : {}/tatami/avatar/{}/{}", tatamiUrl, avatar.getAvatarId(), file.getOriginalFilename()); User user = authenticationService.getCurrentUser(); user.setAvatar(avatar.getAvatarId()); userRepository.updateUser(user); } @RequestMapping(value = "/rest/fileupload", method = RequestMethod.POST) @ResponseBody @Timed public List upload(@RequestParam("uploadFile") MultipartFile file) throws IOException, StorageSizeException { Attachment attachment = new Attachment(); attachment.setContent(file.getBytes()); attachment.setFilename(file.getName()); attachment.setSize(file.getSize()); attachment.setFilename(file.getOriginalFilename()); attachment.setCreationDate(new Date()); attachmentService.createAttachment(attachment); log.debug("Created attachment : {}", attachment.getAttachmentId()); List uploadedFiles = new ArrayList(); UploadedFile uploadedFile = new UploadedFile( attachment.getAttachmentId(), file.getOriginalFilename(), Long.valueOf(file.getSize()).intValue(), tatamiUrl + "/tatami/file/" + attachment.getAttachmentId() + "/" + file.getOriginalFilename()); uploadedFiles.add(uploadedFile); return uploadedFiles; } @RequestMapping(value = "/rest/fileuploadIE", headers = "content-type=multipart/*", method = RequestMethod.POST, produces = "text/html") @ResponseBody @Timed public String uploadIE(@RequestParam("uploadFile") MultipartFile file) throws IOException, StorageSizeException { Attachment attachment = new Attachment(); attachment.setContent(file.getBytes()); attachment.setFilename(file.getName()); attachment.setSize(file.getSize()); attachment.setFilename(file.getOriginalFilename()); attachment.setCreationDate(new Date()); attachmentService.createAttachment(attachment); log.debug("Created attachment : {}", attachment.getAttachmentId()); String result = attachment.getAttachmentId()+":::"+file.getOriginalFilename()+":::"+file.getSize(); return URLEncoder.encode(result, "UTF-8"); } @RequestMapping(value = "/file/file_not_found", method = RequestMethod.GET) @Timed public ModelAndView FileNotFound() { log.debug("File not found !"); return new ModelAndView("errors/file_not_found"); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/fileupload/Message.java ================================================ package fr.ippon.tatami.web.fileupload; import java.io.Serializable; public class Message implements Serializable { private String owner; private String description; private String filename; public Message() { super(); } public Message(String owner, String description, String filename) { super(); this.owner = owner; this.description = description; this.filename = filename; } public String getOwner() { return owner; } public void setOwner(String owner) { this.owner = owner; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getFilename() { return filename; } public void setFilename(String filename) { this.filename = filename; } @Override public String toString() { return "Message{" + "owner='" + owner + '\'' + ", description='" + description + '\'' + ", filename='" + filename + '\'' + '}'; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/fileupload/StatusResponse.java ================================================ package fr.ippon.tatami.web.fileupload; import java.util.ArrayList; import java.util.List; /** * A POJO containing the status of an action and a {@link java.util.List} of messages. * This is mainly used as a DTO for the presentation layer */ public class StatusResponse { private Boolean success; private final List message; public StatusResponse() { this.message = new ArrayList(); } public StatusResponse(Boolean success) { super(); this.success = success; this.message = new ArrayList(); } public StatusResponse(Boolean success, String message) { super(); this.success = success; this.message = new ArrayList(); this.message.add(message); } public StatusResponse(Boolean success, List message) { super(); this.success = success; this.message = message; } public Boolean getSuccess() { return success; } public void setSuccess(Boolean success) { this.success = success; } public List getMessage() { return message; } public void setMessage(String message) { this.message.add(message); } @Override public String toString() { return "StatusResponse{" + "success=" + success + ", message=" + message + '}'; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/fileupload/UploadedFile.java ================================================ package fr.ippon.tatami.web.fileupload; import java.io.Serializable; public class UploadedFile implements Serializable { private String attachmentId; private String name; private Integer size; private String url; private String thumbnail_url; private String delete_url; private String delete_type; public UploadedFile() { super(); } public UploadedFile(String attachmentId, String name, Integer size, String url) { super(); this.attachmentId = attachmentId; this.name = name; this.size = size; this.url = url; } public UploadedFile(String attachmentId, String name, Integer size, String url, String thumbnail_url, String delete_url, String delete_type) { super(); this.attachmentId = attachmentId; this.name = name; this.size = size; this.url = url; this.thumbnail_url = thumbnail_url; this.delete_url = delete_url; this.delete_type = delete_type; } public String getAttachmentId() { return attachmentId; } public void setAttachmentId(String attachmentId) { this.attachmentId = attachmentId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getSize() { return size; } public void setSize(Integer size) { this.size = size; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getThumbnail_url() { return thumbnail_url; } public void setThumbnail_url(String thumbnail_url) { this.thumbnail_url = thumbnail_url; } public String getDelete_url() { return delete_url; } public void setDelete_url(String delete_url) { this.delete_url = delete_url; } public String getDelete_type() { return delete_type; } public void setDelete_type(String delete_type) { this.delete_type = delete_type; } @Override public String toString() { return "UploadedFile{" + "attachmentId='" + attachmentId + '\'' + "name='" + name + '\'' + ", size=" + size + ", url='" + url + '\'' + ", thumbnail_url='" + thumbnail_url + '\'' + ", delete_url='" + delete_url + '\'' + ", delete_type='" + delete_type + '\'' + '}'; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/filter/IeRefreshWrapper.java ================================================ package fr.ippon.tatami.web.filter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; public class IeRefreshWrapper extends HttpServletRequestWrapper { private String accept; public IeRefreshWrapper(HttpServletRequest request) { super(request); if (request.getHeader("accept").equals("*/*")) { accept = "application/x-ms-application, image/jpeg, application/xaml+xml, image/gif, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"; } else { accept = request.getHeader("accept"); } } public String getHeader(String name) { return (name != "Accept" && name != "accept") ? super.getHeader(name) : accept; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/filter/TatamiGzipFilter.java ================================================ package fr.ippon.tatami.web.filter; import net.sf.ehcache.constructs.web.filter.GzipFilter; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Provides GZIP compression of responses, based on ehcache's GzipFilter. */ public class TatamiGzipFilter extends GzipFilter { @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception { if("*/*".equals(request.getHeader("accept"))) { IeRefreshWrapper requestIE = new IeRefreshWrapper(request); chain.doFilter(requestIE, response); } else { //otherwise, continue on in the chain with the ServletRequest and ServletResponse objects chain.doFilter(request, response); } } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/init/WebConfigurer.java ================================================ package fr.ippon.tatami.web.init; import com.yammer.metrics.reporting.AdminServlet; import com.yammer.metrics.web.DefaultWebappMetricsFilter; import fr.ippon.tatami.config.ApplicationConfiguration; import fr.ippon.tatami.config.Constants; import fr.ippon.tatami.config.DispatcherServletConfig; import org.atmosphere.cache.UUIDBroadcasterCache; import org.atmosphere.cpr.AtmosphereServlet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.servlet.DispatcherServlet; import javax.servlet.*; import java.util.EnumSet; /** * Configuration of web application with Servlet 3.0 APIs.
    *

    * This class is to be used as a standard listener within a web.xml. * To optimise startup time, you can completely disable classpath scanning : *

      *
    • with metadata-complete="true" global attribute *
    • with an empty <absolute-ordering> ( it's necessary with Jetty at least TODO : test with other * containers ) *
    *

    * See web.xml : *

    *

     *   <web-app xmlns="http://java.sun.com/xml/ns/javaee"
     *          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     *          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
     *          version="3.0"
     *          metadata-complete="true">
     *
     * 	<!--
     * 	Remove classpath scanning (from servlet 3.0) in order to speed jetty startup :
     * 	metadata-complete="true" above + empty absolute ordering below
     * 	-->
     * 	<absolute-ordering>
     * 		<!--
     * 		 Empty absolute ordering is necessary to completely desactivate classpath scanning
     * 		  -->
     * 	</absolute-ordering>
     *
     *     <display-name>Tatami</display-name>
     *
     *     <!-- All the Servlets and Filters are configured by this ServletContextListener : -->
     *     <listener>
     *         <listener-class>fr.ippon.tatami.web.init.WebConfigurer</listener-class>
     *     </listener>
     *
     * </web-app>
     * 
    * * @author Fabien Arrault */ public class WebConfigurer implements ServletContextListener { private final Logger log = LoggerFactory.getLogger(WebConfigurer.class); @Override public void contextInitialized(ServletContextEvent sce) { ServletContext servletContext = sce.getServletContext(); log.info("Web application configuration"); log.debug("Configuring Spring root application context"); AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); rootContext.register(ApplicationConfiguration.class); rootContext.refresh(); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, rootContext); EnumSet disps = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC); log.debug("Configuring Spring Web application context"); AnnotationConfigWebApplicationContext dispatcherServletConfig = new AnnotationConfigWebApplicationContext(); dispatcherServletConfig.setParent(rootContext); dispatcherServletConfig.register(DispatcherServletConfig.class); log.debug("Registering Spring MVC Servlet"); ServletRegistration.Dynamic dispatcherServlet = servletContext.addServlet("dispatcher", new DispatcherServlet( dispatcherServletConfig)); dispatcherServlet.addMapping("/tatami/*"); dispatcherServlet.setLoadOnStartup(1); log.debug("Registering Spring Security Filter"); FilterRegistration.Dynamic springSecurityFilter = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy()); springSecurityFilter.setAsyncSupported(true); initAtmosphereServlet(servletContext); Environment env = rootContext.getBean(Environment.class); if (env.acceptsProfiles(Constants.SPRING_PROFILE_METRICS)) { initMetricsServlet(servletContext, disps, dispatcherServlet); springSecurityFilter.addMappingForServletNames(disps, true, "dispatcher", "atmosphereServlet", "metricsAdminServlet"); } else { springSecurityFilter.addMappingForServletNames(disps, true, "dispatcher", "atmosphereServlet"); } log.debug("Web application fully configured"); } private void initMetricsServlet(ServletContext servletContext, EnumSet disps, ServletRegistration.Dynamic dispatcherServlet) { log.debug("Setting Metrics profile for the Web ApplicationContext"); log.debug("Registering Metrics Filter"); FilterRegistration.Dynamic metricsFilter = servletContext.addFilter("webappMetricsFilter", new DefaultWebappMetricsFilter()); metricsFilter.addMappingForUrlPatterns(disps, true, "/*"); log.debug("Registering Metrics Admin Servlet"); ServletRegistration.Dynamic metricsAdminServlet = servletContext.addServlet("metricsAdminServlet", new AdminServlet()); metricsAdminServlet.addMapping("/metrics/*"); dispatcherServlet.setLoadOnStartup(2); } private void initAtmosphereServlet(ServletContext servletContext) { log.debug("Registering Atmosphere Servlet"); ServletRegistration.Dynamic atmosphereServlet = servletContext.addServlet("atmosphereServlet", new AtmosphereServlet()); atmosphereServlet.setAsyncSupported(true); atmosphereServlet.setInitParameter("org.atmosphere.cpr.packages", "fr.ippon.tatami.web.atmosphere"); atmosphereServlet.setInitParameter("org.atmosphere.cpr.broadcasterCacheClass", UUIDBroadcasterCache.class.getName()); atmosphereServlet.setInitParameter("org.atmosphere.cpr.broadcaster.shareableThreadPool", "true"); atmosphereServlet.setInitParameter("org.atmosphere.cpr.broadcaster.maxProcessingThreads", "10"); atmosphereServlet.setInitParameter("org.atmosphere.cpr.broadcaster.maxAsyncWriteThreads", "10"); atmosphereServlet.setLoadOnStartup(3); atmosphereServlet.addMapping("/realtime/*"); } @Override public void contextDestroyed(ServletContextEvent sce) { log.info("Destroying Web application"); WebApplicationContext ac = WebApplicationContextUtils.getRequiredWebApplicationContext(sce.getServletContext()); AnnotationConfigWebApplicationContext gwac = (AnnotationConfigWebApplicationContext) ac; gwac.close(); log.debug("Web application destroyed"); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/AccountController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.util.DomainUtil; import fr.ippon.tatami.web.rest.dto.Preferences; import fr.ippon.tatami.web.rest.dto.UserPassword; import org.apache.commons.lang.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.StandardPasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import javax.validation.ConstraintViolationException; /* * REST controller for managing users. * @author Arthur Weber */ @Controller public class AccountController { private static final Logger log = LoggerFactory.getLogger(AccountController.class); @Inject private UserService userService; @Inject private AuthenticationService authenticationService; @Inject Environment env; /* * GET /account/admin -> Determines if the current account is an admin */ @RequestMapping(value = "/rest/account/admin", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public String isAdmin() { log.debug("REST request to determine if user is an admin"); org.springframework.security.core.userdetails.User securityUser = (org.springframework.security.core.userdetails.User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return "{ \"roles\": \"" + securityUser.getAuthorities().toString() + "\" }"; } /** * GET /account/profile -> get account's profile */ @RequestMapping(value = "/rest/account/profile", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public User getProfile() { log.debug("REST request to get account's profile"); User currentUser = authenticationService.getCurrentUser(); return userService.getUserByLogin(currentUser.getLogin()); } /** * PUT /account/profile -> get account's profile */ @RequestMapping(value = "/rest/account/profile", method = RequestMethod.PUT) @ResponseBody @Timed public User updateUserProfile(@RequestBody User updatedUser, HttpServletResponse response) { User currentUser = authenticationService.getCurrentUser(); currentUser.setFirstName(updatedUser.getFirstName().replace("<", " ")); currentUser.setLastName(updatedUser.getLastName().replace("<", " ")); currentUser.setJobTitle(StringEscapeUtils.escapeHtml(updatedUser.getJobTitle().replace("<", " "))); currentUser.setPhoneNumber(updatedUser.getPhoneNumber().replace("<", " ")); try { userService.updateUser(currentUser); } catch (ConstraintViolationException cve) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return null; } log.debug("User updated : {}", currentUser); return currentUser; } @RequestMapping(value = "/rest/account/profile", method = RequestMethod.DELETE) @Timed public void suppressUserProfile() { User currentUser = authenticationService.getCurrentUser(); log.debug("Suppression du compte utilisateur : {}", currentUser); userService.deleteUser(currentUser); } /** * GET /account/preferences -> get account's preferences */ @RequestMapping(value = "/rest/account/preferences", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Preferences getPreferences() { log.debug("REST request to get account's preferences"); User currentUser = authenticationService.getCurrentUser(); User user = userService.getUserByLogin(currentUser.getLogin()); return new Preferences(user); } /** * POST /account/preferences -> update account's preferences */ @RequestMapping(value = "/rest/account/preferences", method = RequestMethod.POST, produces = "application/json") @ResponseBody @Timed public Preferences updatePreferences(@RequestBody Preferences newPreferences, HttpServletResponse response) { log.debug("REST request to set account's preferences"); Preferences preferences = null; try { User currentUser = authenticationService.getCurrentUser(); currentUser.setPreferencesMentionEmail(newPreferences.getMentionEmail()); currentUser.setDailyDigestSubscription(newPreferences.getDailyDigest()); currentUser.setWeeklyDigestSubscription(newPreferences.getWeeklyDigest()); String rssUid = userService.updateRssTimelinePreferences(newPreferences.getRssUidActive()); currentUser.setRssUid(rssUid); preferences = new Preferences(currentUser); userService.updateUser(currentUser); userService.updateDailyDigestRegistration(newPreferences.getDailyDigest()); userService.updateWeeklyDigestRegistration(newPreferences.getWeeklyDigest()); org.springframework.security.core.userdetails.User securityUser = (org.springframework.security.core.userdetails.User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); log.debug("User roles : {}", securityUser); Authentication authentication = new UsernamePasswordAuthenticationToken(securityUser, securityUser.getPassword(), securityUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("User updated : {}", currentUser); } catch (Exception e) { log.debug("Error during setting preferences", e); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } return preferences; } /** * GET /account/password -> throws an error if the password is managed by LDAP */ @RequestMapping(value = "/rest/account/password", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public UserPassword isPasswordManagedByLDAP(HttpServletResponse response) { User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); if (userService.isDomainHandledByLDAP(domain)) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return null; } else { return new UserPassword(); } } /** * GET /account/preferences -> get account's preferences */ @RequestMapping(value = "/rest/account/password", method = RequestMethod.POST, produces = "application/json") @ResponseBody @Timed public UserPassword setPassword(@RequestBody UserPassword userPassword, HttpServletResponse response) { log.debug("REST request to set account's password"); try { User currentUser = authenticationService.getCurrentUser(); StandardPasswordEncoder encoder = new StandardPasswordEncoder(); if (!encoder.matches(userPassword.getOldPassword(), currentUser.getPassword())) { log.debug("The old password is incorrect : {}", userPassword.getOldPassword()); throw new Exception("oldPassword"); } if (!userPassword.getNewPassword().equals(userPassword.getNewPasswordConfirmation())) { throw new Exception("newPasswordConfirmation"); } currentUser.setPassword(userPassword.getNewPassword()); userService.updatePassword(currentUser); log.debug("User password updated : {}", currentUser); return new UserPassword(); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return null; } } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/AttachmentController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.Attachment; import fr.ippon.tatami.service.AttachmentService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; @Controller public class AttachmentController { @Inject private AttachmentService attachmentService; /** * GET /attachments -> get the attachments list */ @RequestMapping(value = "/rest/attachments", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getAttachments( @RequestParam(required = false) Integer pagination, @RequestParam(required = false) String finish) { if (pagination == null) { pagination = 10; } Collection attachmentIds = attachmentService.getAttachmentIdsForCurrentUser(pagination, finish); Collection attachments = new ArrayList(); for (String attachmentId : attachmentIds) { attachments.add(attachmentService.getAttachmentById(attachmentId)); } return attachments; } /** * GET /attachment/{attachmentId} -> get a specific attachment */ @RequestMapping(value = "/rest/attachments/{attachmentId}", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Attachment getAttachmentById(@PathVariable("attachmentId") String attachmentId) { return attachmentService.getAttachmentById(attachmentId); } /** * DELETE /attachment/{attachmentId} -> delete a specific attachment */ @RequestMapping(value = "/rest/attachments/{attachmentId}", method = RequestMethod.DELETE, produces = "application/json") @ResponseBody @Timed public Attachment DeleteAttachment(@PathVariable("attachmentId") String attachmentId) { Attachment attachment = attachmentService.getAttachmentById(attachmentId); attachmentService.deleteAttachment(attachment); return attachment; } /** * GET /attachment/quota -> get quota in % for the domain */ @RequestMapping(value = "/rest/attachments/quota", method = RequestMethod.GET, produces = "application/json") @ResponseBody public Collection getDomainQuota() { return attachmentService.getDomainQuota(); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/BlockController.java ================================================ package fr.ippon.tatami.web.rest; /** * Created by matthieudelafourniere on 7/7/16. */ import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.UserRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.BlockService; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.dto.UserDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import java.util.Collection; /** * REST controller for blocking/unblocking users. */ @Controller public class BlockController { private final Logger log = LoggerFactory.getLogger(BlockController.class); @Inject private BlockService blockService; @Inject UserService userService; @Inject UserRepository userRepository; @Inject private AuthenticationService authenticationService; @RequestMapping(value = "/rest/block/blockedusers/{username}", method = RequestMethod.GET, produces = "application/json") @Timed @ResponseBody public Collection getBlockedUsersForUser(@PathVariable String username, HttpServletResponse response) { User currentUser = authenticationService.getCurrentUser(); if (currentUser == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return null; } String login = username + "@" + currentUser.getDomain(); Collection blockedUsers = blockService.getUsersBlockedForUser(login); return userService.buildUserDTOList(blockedUsers); } /** * Method used by current user. Switch the blocked/unblocked status of the clicked user. * @param username (of the clicked user) * @return */ @RequestMapping(value = "/rest/block/update/{username}", method = RequestMethod.PATCH) @Timed @ResponseBody public UserDTO updateBlockedUser(@PathVariable("username") String username) { User currentUser = authenticationService.getCurrentUser(); String login = username + "@" + currentUser.getDomain(); UserDTO toReturn = userService.buildUserDTO(userRepository.findUserByLogin(login)); if( blockService.isBlocked(currentUser.getLogin(),toReturn.getLogin()) ) { blockService.unblockUser(currentUser.getLogin(), toReturn.getLogin()); } else { blockService.blockUser(currentUser.getLogin(), toReturn.getLogin()); } return toReturn; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/CompanyWallController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.service.TimelineService; import fr.ippon.tatami.service.dto.StatusDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import java.util.Collection; /** * REST controller for getting the company wall. * * @author Julien Dubois */ @Controller public class CompanyWallController { private final Logger log = LoggerFactory.getLogger(CompanyWallController.class); @Inject private TimelineService timelineService; /** * GET /company -> get the public statuses of the current company */ @RequestMapping(value = "/rest/company", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getCompanyWall(@RequestParam(required = false) Integer count, @RequestParam(required = false) String start, @RequestParam(required = false) String finish) { if (count == null) { count = 20; } try { return timelineService.getDomainline(count, start, finish); } catch (NumberFormatException e) { log.warn("Page size undefined ; sizing to default", e); return timelineService.getDomainline(20, start, finish); } } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/FavoritesController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.service.TimelineService; import fr.ippon.tatami.service.dto.StatusDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import java.util.Collection; /** * REST controller for managing favorites. * * @author Julien Dubois */ @Controller public class FavoritesController { private final Logger log = LoggerFactory.getLogger(FavoritesController.class); @Inject private TimelineService timelineService; /** * GET /favorites -> get the favorite status of the current user */ @RequestMapping(value = "/rest/favorites", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection listFavoriteStatus() { log.debug("REST request to get the favorite status of the current user."); return timelineService.getFavoritesline(); } /** * POST /favorites/create/:id -> Favorites the status */ @RequestMapping(value = "/rest/favorites/create/{statusId}", method = RequestMethod.POST) @ResponseBody public void favoriteStatus(@PathVariable("statusId") String statusId) { log.debug("REST request to like status : {}", statusId); timelineService.addFavoriteStatus(statusId); } /** * POST /favorites/destroy/:id -> Unfavorites the status */ @RequestMapping(value = "/rest/favorites/destroy/{statusId}", method = RequestMethod.POST) @ResponseBody public void unfavoriteStatus(@PathVariable("statusId") String statusId) { log.debug("REST request to unlike status : {}", statusId); timelineService.removeFavoriteStatus(statusId); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/FriendshipController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.service.FriendshipService; import fr.ippon.tatami.service.MailService; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.dto.UserDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import java.util.Collection; /** * REST controller for managing friendships. * * @author Julien Dubois */ @Controller public class FriendshipController { private final Logger log = LoggerFactory.getLogger(UserController.class); @Inject private FriendshipService friendshipService; @Inject private UserService userService; @Inject private MailService mailService; @RequestMapping(value = "/rest/users/{username}/friends", method = RequestMethod.GET, produces = "application/json") @Timed @ResponseBody public Collection getFriends(@PathVariable String username, HttpServletResponse response) { User user = userService.getUserByUsername(username); if (user == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return null; } Collection friends = friendshipService.getFriendsForUser(username); return userService.buildUserDTOList(friends); } @RequestMapping(value = "/rest/users/{username}/followers", method = RequestMethod.GET, produces = "application/json") @Timed @ResponseBody public Collection getFollowers(@PathVariable String username, HttpServletResponse response) { User user = userService.getUserByUsername(username); if (user == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return null; } Collection friends = friendshipService.getFollowersForUser(username); return userService.buildUserDTOList(friends); } /** * Added an "action" parameter to specify which type of PATCH we should do (Activate / Follow ). */ @RequestMapping(value = "/rest/users/{username}", method = RequestMethod.PATCH) @Timed @ResponseBody public UserDTO updateFriend(@RequestBody fr.ippon.tatami.web.rest.dto.UserActionStatus action, @PathVariable("username") String username) { if ( action.getFriendShip() != null && action.getFriendShip() ) { if ( action.getFriend() ) { friendshipService.followUser(username); } else { friendshipService.unfollowUser(username); } } else if ( action.getActivate() != null && action.getActivate()) { this.log.debug("REST request to desactivate Profile : {}", username); userService.desactivateUser(username); // User user = userService.getUserByUsername(username); // mailService.sendDeactivatedEmail(user.getLogin()); } return userService.buildUserDTO(userService.getUserByUsername(username)); } /** * WARNING! This is the old API, only used by the admin console *

    * POST /friendships/create -> follow user */ @RequestMapping(value = "/rest/friendships/create", method = RequestMethod.POST, consumes = "application/json") @ResponseBody @Timed @Deprecated public boolean followUser(@RequestBody User user, HttpServletResponse response) { log.debug("REST request to follow username : {}", user.getUsername()); boolean success = friendshipService.followUser(user.getUsername()); if (!success) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } return success; } /** * WARNING! This is the old API, only used by the admin console *

    * POST /friendships/destroy -> unfollow user */ @RequestMapping(value = "/rest/friendships/destroy", method = RequestMethod.POST, consumes = "application/json") @ResponseBody @Timed @Deprecated public boolean unfollowUser(@RequestBody User user, HttpServletResponse response) { log.debug("REST request to unfollow username : {}", user.getUsername()); boolean success = friendshipService.unfollowUser(user.getUsername()); if (!success) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } return success; } /** * WARNING! This is the old API, only used by the admin console *

    * GET /friendships -> is the user a friend ? */ @RequestMapping(value = "/rest/friendships", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed @Deprecated public Boolean followUser(@RequestParam("screen_name") String username) { if (log.isDebugEnabled()) { log.debug("REST request to get friendship status : " + username); } return friendshipService.isFollowing(username); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/GroupController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.GroupService; import fr.ippon.tatami.service.SuggestionService; import fr.ippon.tatami.service.TimelineService; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.dto.StatusDTO; import fr.ippon.tatami.service.dto.UserGroupDTO; import fr.ippon.tatami.service.util.DomainUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.Collection; /** * REST controller for managing groups. * * @author Julien Dubois */ @Controller public class GroupController { private final Logger log = LoggerFactory.getLogger(GroupController.class); @Inject private TimelineService timelineService; @Inject private GroupService groupService; @Inject private AuthenticationService authenticationService; @Inject private UserService userService; @Inject private SuggestionService suggestionService; /** * Get groups of the current user. */ @RequestMapping(value = "/rest/groups", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getGroups() { User currentUser = authenticationService.getCurrentUser(); return groupService.getGroupsForUser(currentUser); } /** * GET /group/:groupId -> returns the group with the requested id */ @RequestMapping(value = "/rest/groups/{groupId}", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Group getGroup(@PathVariable("groupId") String groupId) { User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); Group publicGroup = groupService.getGroupById(domain, groupId); if (publicGroup != null && publicGroup.isPublicGroup()) { Group result = getGroupFromUser(currentUser, groupId); Group groupClone = (Group) publicGroup.clone(); if (result != null) { groupClone.setMember(true); } if (isGroupManagedByCurrentUser(publicGroup)) { groupClone.setAdministrator(true); } return groupClone; } else { Group result = getGroupFromUser(currentUser, groupId); Group groupClone = null; if (result == null) { log.info("Permission denied! User {} tried to access group ID = {} ", currentUser.getLogin(), groupId); return null; } else { groupClone = (Group) result.clone(); groupClone.setMember(true); if (isGroupManagedByCurrentUser(publicGroup)) { groupClone.setAdministrator(true); } } return groupClone; } } /** * PUT /group/:groupId -> update the group with the requested id */ @RequestMapping(value = "/rest/groups/{groupId}", method = RequestMethod.PUT, produces = "application/json") @ResponseBody @Timed public Group updateGroup(@PathVariable("groupId") String groupId, @RequestBody Group groupEdit, HttpServletResponse response) { Group group = getGroup(groupId); if (group != null) { if (!isGroupManagedByCurrentUser(group)) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return null; } else { group.setDomain(authenticationService.getCurrentUser().getDomain()); group.setName(groupEdit.getName()); group.setDescription(groupEdit.getDescription()); group.setArchivedGroup(groupEdit.isArchivedGroup()); groupService.editGroup(group); return group; } } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return null; } } @RequestMapping(value = "/rest/groups/{groupId}/timeline", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection listStatusForGroup(@PathVariable(value = "groupId") String groupId, @RequestParam(required = false) Integer count, @RequestParam(required = false) String start, @RequestParam(required = false) String finish) { log.debug("REST request to get statuses for group : {}", groupId); if (groupId == null) { return new ArrayList(); } if (count == null) { count = 20; } Group group = this.getGroup(groupId); if (group == null) { return new ArrayList(); } else { return timelineService.getGroupline(groupId, count, start, finish); } } /** * GET /groupmemberships/lookup -> return extended data about the user's groups */ @RequestMapping(value = "/rest/groupmemberships/lookup", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getUserGroups(@RequestParam("screen_name") String username) { User user = userService.getUserByUsername(username); if (user == null) { log.debug("Trying to find group for non-existing username = {}", username); return new ArrayList(); } return groupService.getGroupsForUser(user); } /** * Get groups where the current user is admin. */ @RequestMapping(value = "/rest/admin/groups", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getAdminGroups() { return groupService.getGroupsWhereCurrentUserIsAdmin(); } /** * POST create new group. */ @RequestMapping(value = "/rest/groups", method = RequestMethod.POST, produces = "application/json") @ResponseBody @Timed public Group createGroup(HttpServletResponse response, @RequestBody Group group) { if (group.getName() != null && !group.getName().equals("")) { groupService.createGroup(group.getName(), group.getDescription(), group.isPublicGroup()); } else { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } return group; } /** * GET /groupmemberships/suggestions -> suggest groups to join */ @RequestMapping(value = "/rest/groupmemberships/suggestions", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection suggestions() { String login = authenticationService.getCurrentUser().getLogin(); return groupService.buildGroupList(suggestionService.suggestGroups(login)); } /** * GET /groups/{groupId}/members/ -> members of the group */ @RequestMapping(value = "/rest/groups/{groupId}/members/", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getGroupsUsers(HttpServletResponse response, @PathVariable("groupId") String groupId) { User currentUser = authenticationService.getCurrentUser(); Group currentGroup = groupService.getGroupById(currentUser.getDomain(), groupId); Collection users = null; if (currentUser == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Authentication required } else if (currentGroup == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); // Resource not found } else { users = groupService.getMembersForGroup(groupId, currentUser.getLogin()); } return users; } /** * GET /groups/{groupId}/members/{userUsername} -> get a member to group status */ @RequestMapping(value = "/rest/groups/{groupId}/members/{username}", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public UserGroupDTO getUserToGroup(HttpServletResponse response, @PathVariable("groupId") String groupId, @PathVariable("username") String username) { User currentUser = authenticationService.getCurrentUser(); Group currentGroup = groupService.getGroupById(currentUser.getDomain(), groupId); Collection users = null; if (currentUser == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Authentication required } else if (currentGroup == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); // Resource not found } else { users = groupService.getMembersForGroup(groupId, currentUser.getLogin()); } for (UserGroupDTO user : users) { if (user.getLogin().equals(currentUser.getLogin())) { return user; } } UserGroupDTO currentUserDTO = new UserGroupDTO(); currentUserDTO.setLogin(currentUser.getLogin()); currentUserDTO.setUsername(currentUser.getUsername()); currentUserDTO.setAvatar(currentUser.getAvatar()); currentUserDTO.setFirstName(currentUser.getFirstName()); currentUserDTO.setLastName(currentUser.getLastName()); currentUserDTO.setIsMember(false); return currentUserDTO; } /** * PUT /groups/{groupId}/members/{userUsername} -> add a member to group */ @RequestMapping(value = "/rest/groups/{groupId}/members/{username}", method = RequestMethod.PUT, produces = "application/json") @ResponseBody @Timed public UserGroupDTO addUserToGroup(HttpServletResponse response, @PathVariable("groupId") String groupId, @PathVariable("username") String username) { User currentUser = authenticationService.getCurrentUser(); Group currentGroup = groupService.getGroupById(currentUser.getDomain(), groupId); User userToAdd = userService.getUserByUsername(username); UserGroupDTO dto = null; if (currentUser == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Authentication required } else if (currentGroup == null || userToAdd == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); // Resource not found } else { if (isGroupManagedByCurrentUser(currentGroup) && !currentUser.equals(userToAdd)) { groupService.addMemberToGroup(userToAdd, currentGroup); dto = groupService.getMembersForGroup(groupId, userToAdd); } else if (currentGroup.isPublicGroup() && currentUser.equals(userToAdd) && !isGroupManagedByCurrentUser(currentGroup)) { groupService.addMemberToGroup(userToAdd, currentGroup); dto = groupService.getMembersForGroup(groupId, userToAdd); } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); } } return dto; } /** * DELETE /groups/{groupId}/members/{userUsername} -> remove a member to group */ @RequestMapping(value = "/rest/groups/{groupId}/members/{username}", method = RequestMethod.DELETE, produces = "application/json") @ResponseBody @Timed public boolean removeUserFromGroup(HttpServletResponse response, @PathVariable("groupId") String groupId, @PathVariable("username") String username) { User currentUser = authenticationService.getCurrentUser(); Group currentGroup = groupService.getGroupById(currentUser.getDomain(), groupId); User userToremove = userService.getUserByUsername(username); UserGroupDTO dto = null; if (currentUser == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Authentication required return false; } else if (currentGroup == null || userToremove == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); // Resource not found return false; } else { if (isGroupManagedByCurrentUser(currentGroup) && !currentUser.equals(userToremove)) { groupService.removeMemberFromGroup(userToremove, currentGroup); groupService.getMembersForGroup(groupId, userToremove); } else if (currentGroup.isPublicGroup() && currentUser.equals(userToremove) && !isGroupManagedByCurrentUser(currentGroup)) { groupService.removeMemberFromGroup(userToremove, currentGroup); groupService.getMembersForGroup(groupId, userToremove); } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return false; } } return true; } private boolean isGroupManagedByCurrentUser(Group group) { Collection groups = groupService.getGroupsWhereCurrentUserIsAdmin(); boolean isGroupManagedByCurrentUser = false; for (Group testGroup : groups) { if (testGroup.getGroupId().equals(group.getGroupId())) { isGroupManagedByCurrentUser = true; break; } } return isGroupManagedByCurrentUser; } private Group getGroupFromUser(User currentUser, String groupId) { Collection groups = groupService.getGroupsForUser(currentUser); for (Group testGroup : groups) { if (testGroup.getGroupId().equals(groupId)) { return testGroup; } } return null; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/MentionsController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.service.TimelineService; import fr.ippon.tatami.service.dto.StatusDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import java.util.Collection; /** * REST controller for getting the mention line. * * @author Julien Dubois */ @Controller public class MentionsController { private final Logger log = LoggerFactory.getLogger(MentionsController.class); @Inject private TimelineService timelineService; /** * GET /mentions -> get the mentions for the current user */ @RequestMapping(value = "/rest/mentions", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection listMentionStatus(@RequestParam(required = false) Integer count, @RequestParam(required = false) String start, @RequestParam(required = false) String finish) { if (count == null) { count = 20; } try { return timelineService.getMentionline(count, start, finish); } catch (NumberFormatException e) { log.warn("Page size undefined ; sizing to default", e); return timelineService.getMentionline(20, start, finish); } } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/SearchController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.UserTagRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.*; import fr.ippon.tatami.service.dto.StatusDTO; import fr.ippon.tatami.service.dto.UserDTO; import fr.ippon.tatami.service.util.DomainUtil; import fr.ippon.tatami.web.rest.dto.SearchResults; import fr.ippon.tatami.web.rest.dto.Tag; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; /** * Search engine controller. */ @Controller public class SearchController { private final Logger log = LoggerFactory.getLogger(SearchController.class); @Inject private AuthenticationService authenticationService; @Inject private SearchService searchService; @Inject private TimelineService timelineService; @Inject private UserService userService; @Inject private GroupService groupService; @Inject private TrendService trendService; @Inject private UserTagRepository userTagRepository; /** * GET /search/all?q=tatami -> search users, tags, groups for "tatami" */ @RequestMapping(value = "/rest/search/all", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public SearchResults search(@RequestParam(value = "q", required = false, defaultValue = "") String q) { SearchResults searchResults = new SearchResults(); searchResults.setTags(this.searchRecentTags(q)); searchResults.setUsers(this.searchUsers(q)); searchResults.setGroups(this.searchGroups(q)); return searchResults; } /** * GET /search/status?q=tatami -> get the status where "tatami" appears */ @RequestMapping(value = "/rest/search/status", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection listStatusForUser(@RequestParam(value = "q", required = false, defaultValue = "") String query, @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, @RequestParam(value = "rpp", required = false, defaultValue = "20") Integer rpp) { log.debug("REST request to search status containing these words ({}).", query); final User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); List line; if (StringUtils.isNotBlank(query)) { line = searchService.searchStatus(domain, query, page, rpp); } else { line = Collections.emptyList(); } return timelineService.buildStatusList(line); } /** * GET /search/tags" -> search tags
    * * @return a Collection of tags matching the query */ @RequestMapping(value = "/rest/search/tags", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection searchRecentTags(@RequestParam("q") String query) { String prefix = query.toLowerCase(); String currentLogin = authenticationService.getCurrentUser().getLogin(); String domain = DomainUtil.getDomainFromLogin(currentLogin); Collection followedTags = userTagRepository.findTags(currentLogin); Collection trends = trendService.searchTags(domain, prefix, 5); Collection tags = new ArrayList(); if (query != null && !query.equals("")) { this.log.debug("REST request to find tags starting with : {}", prefix); for (String trend : trends) { Tag tag = new Tag(); tag.setName(trend); if (followedTags.contains(trend)) { tag.setFollowed(true); } tags.add(tag); } } return tags; } /** * GET /search/groups" -> search groups
    * * @return a Collection of groups matching the query */ @RequestMapping(value = "/rest/search/groups", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection searchGroups(@RequestParam("q") String query) { String prefix = query.toLowerCase(); String currentLogin = authenticationService.getCurrentUser().getLogin(); String domain = DomainUtil.getDomainFromLogin(currentLogin); Collection groups; if (query != null && !query.equals("")) { this.log.debug("REST request to find groups starting with : {}", prefix); groups = searchService.searchGroupByPrefix(domain, prefix, 5); } else { groups = new ArrayList(); } return groupService.buildGroupList(groups); } /** * GET /search/users" -> search user by username
    * Should return a collection of users matching the query.
    * The collection doesn't contain the current user even if he matches the query.
    * If nothing matches, an empty collection (but not null) is returned.
    * * @param query the query * @return a Collection of User */ @RequestMapping(value = "/rest/search/users", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection searchUsers(@RequestParam("q") String query) { String prefix = query.toLowerCase(); User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); Collection logins = searchService.searchUserByPrefix(domain, prefix); Collection users; if (query != null && !query.equals("")) { this.log.debug("REST request to find users starting with : {}", prefix); users = userService.getUsersByLogin(logins); } else { users = new ArrayList(); } return userService.buildUserDTOList(users); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/StatsController.java ================================================ package fr.ippon.tatami.web.rest; import fr.ippon.tatami.domain.UserStatusStat; import fr.ippon.tatami.service.StatsService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import java.util.Collection; /** * REST controller for managing stats. * * @author Julien Dubois */ @Controller public class StatsController { private final Logger log = LoggerFactory.getLogger(StatsController.class); @Inject private StatsService statsService; /** * GET /stats/day -> statistics for today */ @RequestMapping(value = "/rest/stats/day", method = RequestMethod.GET, produces = "application/json") @ResponseBody public Collection listDayStatusStats() { log.debug("REST request to get the users stats."); return statsService.getDayline(); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/TagController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.UserTagRepository; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.TagMembershipService; import fr.ippon.tatami.service.TimelineService; import fr.ippon.tatami.service.TrendService; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.dto.StatusDTO; import fr.ippon.tatami.service.util.DomainUtil; import fr.ippon.tatami.web.rest.dto.Tag; import fr.ippon.tatami.web.rest.dto.Trend; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * REST controller for managing tags. * * @author Julien Dubois */ @Controller public class TagController { private final Logger log = LoggerFactory.getLogger(TagController.class); @Inject private TimelineService timelineService; @Inject private TagMembershipService tagMembershipService; @Inject private TrendService trendService; @Inject private UserTagRepository userTagRepository; @Inject private AuthenticationService authenticationService; @Inject private UserService userService; /** * GET /rest/tags/{tagName}/tag_timeline -> get the latest status for a given tag */ @RequestMapping(value = "/rest/tags/{tagName}/tag_timeline", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection listStatusForTag(@PathVariable String tagName, @RequestParam(required = false) Integer count, @RequestParam(required = false) String start, @RequestParam(required = false) String finish) { log.debug("REST request to get statuses for tag : {}", tagName); if (count == null) { count = 20; } try { return timelineService.getTagline(tagName, count, start, finish); } catch (NumberFormatException e) { log.warn("Page size undefined ; sizing to default", e); return timelineService.getTagline(tagName, 20, start, finish); } } /** * WARNING! This is the old API, only used by the admin console *

    * POST /tagmemberships/create -> follow tag */ @RequestMapping(value = "/rest/tagmemberships/create", method = RequestMethod.POST, consumes = "application/json") @ResponseBody @Timed @Deprecated public boolean followTag(@RequestBody Tag tag) { log.debug("REST request to follow tag : {}", tag); return tagMembershipService.followTag(tag); } /** * WARNING! This is the old API, only used by the admin console *

    * POST /tagmemberships/destroy -> unfollow tag */ @RequestMapping(value = "/rest/tagmemberships/destroy", method = RequestMethod.POST, consumes = "application/json") @ResponseBody @Timed @Deprecated public boolean unfollowTag(@RequestBody Tag tag) { log.debug("REST request to unfollow tag : {}", tag); return tagMembershipService.unfollowTag(tag); } /** * POST /tagmemberships/lookup -> looks up the tag for the user */ @RequestMapping(value = "/rest/tagmemberships/lookup", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Tag lookupTag(@RequestParam("tag_name") String tagname) { User currentUser = authenticationService.getCurrentUser(); Collection followedTags = userTagRepository.findTags(currentUser.getLogin()); Tag tag = new Tag(); tag.setName(tagname); if (followedTags.contains(tagname)) { tag.setFollowed(true); } return tag; } /** * GET /tagmemberships/list -> get the tags followed by the current user */ @RequestMapping(value = "/rest/tagmemberships/list", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getFollowedTags() { User currentUser = authenticationService.getCurrentUser(); Collection followedTags = userTagRepository.findTags(currentUser.getLogin()); Collection tags = new ArrayList(); for (String followedTag : followedTags) { Tag tag = new Tag(); tag.setName(followedTag); tag.setFollowed(true); tags.add(tag); } return tags; } /** * GET /tags/popular -> get the list of popular tags */ @RequestMapping(value = "/rest/tags/popular", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getPopularTags() { User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); List trends = trendService.getCurrentTrends(domain); Collection followedTags = userTagRepository.findTags(currentUser.getLogin()); Collection tags = new ArrayList(); for (Trend trend : trends) { Tag tag = new Tag(); tag.setName(trend.getTag()); if (followedTags.contains(trend.getTag())) { tag.setFollowed(true); } tags.add(tag); } return tags; } @RequestMapping(value = "/rest/tags", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getTags(@RequestParam(required = false, value = "popular") String popular, @RequestParam(required = false, value = "user") String username, @RequestParam(required = false, value = "search") String search) { Collection tags = new ArrayList(); User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); Collection followedTags = userTagRepository.findTags(currentUser.getLogin()); Collection tagNames; if (popular != null) { List trends; User user = null; if (username != null) user = userService.getUserByUsername(username); if (user != null) { trendService.getTrendsForUser(user.getLogin()); trends = trendService.getTrendsForUser(user.getLogin()); } else { trends = trendService.getCurrentTrends(domain); } for (Trend trend : trends) { Tag tag = new Tag(); tag.setName(trend.getTag()); tag.setTrendingUp(trend.isTrendingUp()); tags.add(tag); } } else if (search != null && !search.isEmpty()) { String prefix = search.toLowerCase(); tagNames = trendService.searchTags(domain, prefix, 5); for (String tagName : tagNames) { Tag tag = new Tag(); tag.setName(tagName); tags.add(tag); } } else { tagNames = userTagRepository.findTags(currentUser.getLogin()); for (String tagName : tagNames) { Tag tag = new Tag(); tag.setName(tagName); tags.add(tag); } } for (Tag tag : tags) { if (followedTags.contains(tag.getName())) { tag.setFollowed(true); } } return tags; } @RequestMapping(value = "/rest/tags/{tag}", method = RequestMethod.PUT, produces = "application/json") @ResponseBody @Timed public Tag updateTag(@RequestBody Tag tag) { if (tag.isFollowed()) { tagMembershipService.followTag(tag); } else { tagMembershipService.unfollowTag(tag); } return tag; } @RequestMapping(value = "/rest/tags/{tag}", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Tag getTag(@PathVariable("tag") String tagName) { User currentUser = authenticationService.getCurrentUser(); Collection followedTags = userTagRepository.findTags(currentUser.getLogin()); Tag tag = new Tag(); tag.setName(tagName); if (followedTags.contains(tagName)) { tag.setFollowed(true); } return tag; } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/TimelineController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.domain.status.AbstractStatus; import fr.ippon.tatami.domain.status.StatusDetails; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.GroupService; import fr.ippon.tatami.service.StatusUpdateService; import fr.ippon.tatami.service.TimelineService; import fr.ippon.tatami.service.dto.StatusDTO; import fr.ippon.tatami.service.exception.ArchivedGroupException; import fr.ippon.tatami.service.exception.ReplyStatusException; import fr.ippon.tatami.web.rest.dto.ActionStatus; import org.apache.commons.lang.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import twitter4j.StatusUpdate; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; /** * REST controller for managing status. * * @author Julien Dubois */ @Controller public class TimelineController { private final Logger log = LoggerFactory.getLogger(TimelineController.class); @Inject private TimelineService timelineService; @Inject private StatusUpdateService statusUpdateService; @Inject private GroupService groupService; @Inject private AuthenticationService authenticationService; /** * GET /statuses/details/:id -> returns the details for a status, specified by the id parameter */ @RequestMapping(value = "/rest/statuses/details/{statusId}", method = RequestMethod.GET, produces = "application/json") @ResponseBody public StatusDetails getStatusDetails(@PathVariable("statusId") String statusId) { log.debug("REST request to get status details Id : {}", statusId); return timelineService.getStatusDetails(statusId); } /** * Report a status */ @RequestMapping(value = "/rest/statuses/report/{statusId}", method = RequestMethod.POST) @ResponseBody public void reportStatus(@PathVariable("statusId") String statusId) { log.debug("REST request to report a status details Id : {}", statusId); statusUpdateService.reportStatus(authenticationService.getCurrentUser().getLogin(), statusId); } /** * Report a status */ @RequestMapping(value = "/rest/statuses/report/reportedList", method = RequestMethod.GET, produces = "application/json") @ResponseBody public Collection getReportStatuses() { log.debug("REST request to get all reports"); Collection reportedStatusList = statusUpdateService.findReportedStatuses(); return reportedStatusList; } @RequestMapping(value = "/rest/statuses/report/{statusId}", method = RequestMethod.PUT) @ResponseBody public void deleteReportedStatus(@PathVariable("statusId") String statusId) { log.debug("REST request to delete a status Id : {}", statusId); statusUpdateService.deleteReportedStatus(statusId); } @RequestMapping(value = "/rest/statuses/report/{statusId}", method = RequestMethod.DELETE) @ResponseBody public void approveReportedStatus(@PathVariable("statusId") String statusId) { log.debug("REST request to approve a status Id : {}", statusId); statusUpdateService.approveReportedStatus(statusId); } /** * GET /statuses/home_timeline -> get the latest statuses from the current user */ @RequestMapping(value = "/rest/statuses/home_timeline", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection listStatus(@RequestParam(required = false) Integer count, @RequestParam(required = false) String start, @RequestParam(required = false) String finish) { if (count == null || count == 0) { count = 20; //Default value } try { return timelineService.getTimeline(count, start, finish); } catch (Exception e) { StringWriter stack = new StringWriter(); PrintWriter pw = new PrintWriter(stack); e.printStackTrace(pw); log.debug("{}", stack.toString()); return null; } } /** * GET /statuses/user_timeline?screen_name=jdubois -> get the latest statuses from user "jdubois" */ @RequestMapping(value = "/rest/statuses/{username}/timeline", method = RequestMethod.GET, produces = "application/json") @ResponseBody public Collection listStatusForUser(@PathVariable("username") String username, @RequestParam(required = false) Integer count, @RequestParam(required = false) String start, @RequestParam(required = false) String finish) { if (count == null || count == 0) { count = 20; //Default value } log.debug("REST request to get someone's status (username={}).", username); if (username == null || username.length() == 0) { return new ArrayList(); } try { return timelineService.getUserline(username, count, start, finish); } catch (Exception e) { if (log.isDebugEnabled()) { e.printStackTrace(); } return new ArrayList(); } } @RequestMapping(value = "/rest/statuses/{statusId}", method = RequestMethod.GET, produces = "application/json") @ResponseBody public StatusDTO getStatus(@PathVariable("statusId") String statusId) { log.debug("REST request to get status Id : {}", statusId); return timelineService.getStatus(statusId); } @RequestMapping(value = "/rest/statuses/{statusId}", method = RequestMethod.DELETE) public void deleteStatus(@PathVariable("statusId") String statusId) { log.debug("REST request to get status Id : {}", statusId); timelineService.removeStatus(statusId); } @RequestMapping(value = "/rest/statuses/{statusId}", method = RequestMethod.PATCH) @ResponseBody public StatusDTO updateStatus(@RequestBody ActionStatus action, @PathVariable("statusId") String statusId) { try { StatusDTO status = timelineService.getStatus(statusId); if (action.isFavorite() != null && status.isFavorite() != action.isFavorite()) { if (action.isFavorite()) { timelineService.addFavoriteStatus(statusId); } else { timelineService.removeFavoriteStatus(statusId); } status.setFavorite(action.isFavorite()); } if (action.isShared() != null && action.isShared()) { timelineService.shareStatus(statusId); status.setShareByMe(action.isShared()); } if (action.isAnnounced() != null && action.isAnnounced()) { timelineService.announceStatus(statusId); } return status; } catch (Exception e) { StringWriter stack = new StringWriter(); PrintWriter pw = new PrintWriter(stack); e.printStackTrace(pw); log.debug("{}", stack.toString()); return null; } } /** * POST /statuses/ -> create a new Status */ @RequestMapping(value = "/rest/statuses/", method = RequestMethod.POST, produces = "application/json") @Timed public String postStatus(@RequestBody StatusDTO status, HttpServletResponse response) throws ArchivedGroupException, ReplyStatusException { log.debug("REST request to add status : {}", status.getContent()); String escapedContent = StringEscapeUtils.escapeHtml(status.getContent()); Collection attachmentIds = status.getAttachmentIds(); if (status.getReplyTo() != null && !status.getReplyTo().isEmpty()) { log.debug("Creating a reply to : {}", status.getReplyTo()); statusUpdateService.replyToStatus(escapedContent, status.getReplyTo(), attachmentIds); } else if (status.isStatusPrivate() || status.getGroupId() == null || status.getGroupId().equals("")) { log.debug("Private status"); statusUpdateService.postStatus(escapedContent, status.isStatusPrivate(), attachmentIds, status.getGeoLocalization()); } else { User currentUser = authenticationService.getCurrentUser(); Collection groups = groupService.getGroupsForUser(currentUser); Group group = null; for (Group testGroup : groups) { if (testGroup.getGroupId().equals(status.getGroupId())) { group = testGroup; break; } } if (group == null) { log.info("Permission denied! User {} tried to access " + "group ID = {}", currentUser.getLogin(), status.getGroupId()); response.setStatus(HttpServletResponse.SC_FORBIDDEN); } else if (group.isArchivedGroup()) { log.info("Archived group! User {} tried to post a message to archived " + "group ID = {}", currentUser.getLogin(), status.getGroupId()); response.setStatus(HttpServletResponse.SC_FORBIDDEN); } else { statusUpdateService.postStatusToGroup(escapedContent, group, attachmentIds, status.getGeoLocalization()); } } return "{}"; } /** * POST /statuses/hide -> hide the status from the current user timeline */ @RequestMapping(value = "/rest/statuses/hide/{statusId}", method = RequestMethod.POST) public void hideStatus(@PathVariable("statusId") String statusId) { log.debug("REST request to hide status Id : {}", statusId); timelineService.hideStatus(statusId); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/TrendController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.TrendService; import fr.ippon.tatami.service.util.DomainUtil; import fr.ippon.tatami.web.rest.dto.Trend; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import java.util.List; /** * REST controller for managing trends. * * @author Julien Dubois */ @Controller public class TrendController { private final Logger log = LoggerFactory.getLogger(TrendController.class); @Inject private AuthenticationService authenticationService; @Inject private TrendService trendService; /** * GET /trends -> get the tag trends */ @RequestMapping(value = "/rest/trends", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public List getTrends() { String currentLogin = authenticationService.getCurrentUser().getLogin(); String domain = DomainUtil.getDomainFromLogin(currentLogin); return trendService.getCurrentTrends(domain); } /** * GET /users/trends -> get the user trends */ @RequestMapping(value = "/rest/user/trends", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public List getUserTrends(@RequestParam("screen_name") String username) { String currentLogin = authenticationService.getCurrentUser().getLogin(); String domain = DomainUtil.getDomainFromLogin(currentLogin); return trendService.getTrendsForUser(DomainUtil.getLoginFromUsernameAndDomain(username, domain)); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/UserController.java ================================================ package fr.ippon.tatami.web.rest; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.Group; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.security.AuthenticationService; import fr.ippon.tatami.service.SearchService; import fr.ippon.tatami.service.SuggestionService; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.dto.UserDTO; import fr.ippon.tatami.service.util.DomainUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import java.util.Collection; /** * REST controller for managing users. * * @author Julien Dubois */ @Controller public class UserController { private final Logger log = LoggerFactory.getLogger(UserController.class); @Inject private UserService userService; @Inject private AuthenticationService authenticationService; @Inject private SearchService searchService; @Inject private SuggestionService suggestionService; /** * GET /rest/users/:username -> get the "jdubois" user */ @RequestMapping(value = "/rest/users/{username}", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public UserDTO getUser(@PathVariable("username") String username) { this.log.debug("REST request to get Profile : {}", username); User user = userService.getUserByUsername(username); return userService.buildUserDTO(user); } /** * GET /users/suggestions -> suggest users to follow */ @RequestMapping(value = "/rest/users/suggestions", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection suggestions() { String login = authenticationService.getCurrentUser().getLogin(); return suggestionService.suggestUsers(login); } /** * GET /rest/users/search -> search users by prefix
    * Should return a collection of users matching the query.
    * The collection doesn't contain the current user even if he matches the query.
    * If nothing matches, an empty collection (but not null) is returned.
    * * @param query the query * @return a Collection of User */ @RequestMapping(value = "/rest/users/search", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection searchUsers(@RequestParam("q") String query) { String prefix = query.toLowerCase(); this.log.debug("REST request to find users starting with : {}", prefix); User currentUser = authenticationService.getCurrentUser(); String domain = DomainUtil.getDomainFromLogin(currentUser.getLogin()); Collection logins = searchService.searchUserByPrefix(domain, prefix); return userService.getUsersByLogin(logins); } /** * GET /users -> Get all users of domain */ @RequestMapping(value = "/rest/users", method = RequestMethod.GET, produces = "application/json") @ResponseBody @Timed public Collection getAll(@RequestParam(required = false) Integer pagination) { if (pagination == null) { pagination = 0; } return userService.buildUserDTOList(userService.getUsersForCurrentDomain(pagination)); } /** * POST /users -> Register new user */ @RequestMapping(value = "/rest/users", method = RequestMethod.POST, produces = "application/json") @ResponseBody public void register(@RequestParam String email, HttpServletResponse response) { email = email.toLowerCase(); if (userService.getUserByLogin(email) != null) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } User user = new User(); user.setLogin(email); userService.registerUser(user); response.setStatus(HttpServletResponse.SC_CREATED); } } ================================================ FILE: web/src/main/java/fr/ippon/tatami/web/rest/UserXAuthController.java ================================================ package fr.ippon.tatami.web.rest; import com.google.api.client.auth.oauth2.TokenResponse; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.services.plus.Plus; import com.google.api.services.plus.model.Person; import com.yammer.metrics.annotation.Timed; import fr.ippon.tatami.domain.User; import fr.ippon.tatami.repository.DomainRepository; import fr.ippon.tatami.security.GoogleAuthenticationToken; import fr.ippon.tatami.security.xauth.Token; import fr.ippon.tatami.security.xauth.TokenProvider; import fr.ippon.tatami.service.UserService; import fr.ippon.tatami.service.util.DomainUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import javax.inject.Inject; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @Controller public class UserXAuthController { private static final Logger log = LoggerFactory.getLogger(UserXAuthController.class); private static final String GOOGLE_AUTH_CODE_HEADER_NAME = "x-auth-code-header"; @Inject private UserService userService; @Inject private TokenProvider tokenProvider; @Inject private UserDetailsService userDetailsService; @Inject private DomainRepository domainRepository; @Inject private AuthenticationManager authenticationManager; @Inject Environment env; /** * GET /rest/client/id -> Gets the client id */ @RequestMapping(value = "/rest/client/id", method = RequestMethod.GET, produces = "application/json") @Timed public Collection getClientId() { String clientId = env.getProperty("tatami.google.clientId"); Collection clientList = new ArrayList(); clientList.add(clientId); return clientList; } /** * POST /rest/oauth/token -> Gets a token based on the users google information */ @RequestMapping(value = "/rest/oauth/token", method = RequestMethod.POST) @Timed public Token getGoogleUser(ServletRequest servletRequest) { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String authorizationCode = httpServletRequest.getHeader(GOOGLE_AUTH_CODE_HEADER_NAME); Token authToken = null; if(StringUtils.hasText(authorizationCode)){ try { Person user = getGoogleUserInfo(authorizationCode); UserDetails userDetails = getUserDetails(user); GoogleAuthenticationToken token = new GoogleAuthenticationToken(userDetails); authToken = tokenProvider.createToken(userDetails); Authentication authentication = authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (IOException ioe) { log.error("{}", ioe); } } return authToken; } @RequestMapping(value = "/rest/authentication", method = RequestMethod.POST) @Timed public Token authorize(@RequestParam String j_username, @RequestParam String j_password) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(j_username, j_password); Authentication authentication = authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails details = userDetailsService.loadUserByUsername(j_username); return tokenProvider.createToken(details); } private Person getGoogleUserInfo(String authorizationCode) throws IOException { String clientId = env.getProperty("tatami.google.clientId"); String clientSecret = env.getProperty("tatami.google.clientSecret"); HttpTransport transport = new NetHttpTransport(); JacksonFactory jacksonFactory = new JacksonFactory(); TokenResponse accessCode = new GoogleAuthorizationCodeTokenRequest(transport, jacksonFactory, clientId, clientSecret, authorizationCode, "http://localhost/callback") .execute(); GoogleCredential googleCredential = new GoogleCredential.Builder() .setJsonFactory(new JacksonFactory()) .setTransport(transport) .setClientSecrets(clientId, clientSecret) .build() .setFromTokenResponse(accessCode); Plus plus = new Plus.Builder(transport, jacksonFactory, googleCredential) .setApplicationName("Tatami") .build(); return plus.people().get("me").execute(); } private UserDetails getUserDetails(Person user) throws UsernameNotFoundException { String login = user.getEmails().get(0).getValue(); if(login == null) { String msg = "OAuth response did not contain the user email"; log.error(msg); throw new UsernameNotFoundException(msg); } if(!login.contains("@")) { log.debug("User login {} from OAuth response is incorrect.", login); throw new UsernameNotFoundException("OAuth response did not contains a valid user email"); } UserDetails userDetails; try { userDetails = userDetailsService.loadUserByUsername(login); domainRepository.updateUserInDomain(DomainUtil.getDomainFromLogin(login), login); } catch (UsernameNotFoundException e) { log.info("User with login : \"{}\" doesn't exist yet in Tatami database - creating it...", login); userDetails = getNewlyCreatedUserDetails(user); } return userDetails; } private UserDetails getNewlyCreatedUserDetails(Person user) { String login = user.getEmails().get(0).getValue(); String firstName = user.getName().getGivenName(); String lastName = user.getName().getFamilyName(); User createdUser = new User(); createdUser.setLogin(login); createdUser.setFirstName(firstName); createdUser.setLastName(lastName); userService.createUser(createdUser); return userDetailsService.loadUserByUsername(createdUser.getLogin()); } } ================================================ FILE: web/src/main/webapp/.bowerrc ================================================ { "directory" : "assets/bower_components" } ================================================ FILE: web/src/main/webapp/WEB-INF/messages/messages_en.properties ================================================ tatami.copyright=Copyright 2012 tatami.ippon.technologies=Ippon Technologies tatami.github.fork=Fork Tatami on Github tatami.github.issues=Submit a bug tatami.ippon.website=Ippon Technologies Website tatami.ippon.blog=Ippon Technologies Blog tatami.ippon.twitter.follow=Follow @ippontech on Twitter tatami.title=Tatami tatami.profile=Profile tatami.home=Home tatami.menu.about=About tatami.menu.presentation=Presentation tatami.menu.tos=Terms of service tatami.menu.language=Language tatami.menu.language.en=English tatami.menu.language.fr=Francais tatami.menu.license=Source code license tatami.search.placeholder=Search tatami.search.button=Search tatami.search.filter=Enter your search tatami.logout=Logout tatami.login=E-mail tatami.username=User name tatami.password=Password tatami.404=Page not found. tatami.500=An error has occurred. tatami.file.not.found=The requested file does not exist. It was probably deleted by its owner. tatami.form.success=The form has been successfully saved. tatami.form.error=Failed to saved form. tatami.login.modal.timeout.title=Warning tatami.login.modal.timeout.message=You were disconnected. Sign in again to use Tatami. tatami.login.modal.timeout.close=Close tatami.register.title=You don't have an account (yet) tatami.register.text.1=To create an account in Tatami, please register your e-mail address. tatami.register.text.2=A confirmation message will be sent to the e-mail address you provided. tatami.register.text.3=Depending on your e-mail address' domain name, you will join your company's private space. For example, users with an email@ippon.fr address will join Ippon's private space tatami.register.text.4=If you are the first employee of your company to join Tatami, your company's private space will be automatically created. tatami.register=Register tatami.register.msg=Thank you! A registration e-mail has been sent to you. tatami.register.msg.error=This e-mail address is already in used by a Tatami user. tatami.register.validation.title=E-mail validation tatami.register.validation.error=You e-mail could not be validated. Please use the registration form to register your e-mail. tatami.register.validation.ok=Your e-mail has been validated. Your password will be e-mailed to you. tatami.register.home=Go to the home page tatami.lost.password.title=Have you forgotten your password? tatami.lost.password.button=Ask for a new password tatami.lost.password.msg=An e-mail has been sent to you, with instructions to generate a new password. tatami.lost.password.msg.error=This e-mail address is not registered in Tatami. tatami.ldap.password.msg.error=This account is managed by your LDAP server, you cannot generate a new password with Tatami. tatami.authentification=You already have an account tatami.authentificate=Authenticate tatami.authentification.error=Your authentication has failed! Are you sure you used the correct password? tatami.authentication.google.title=Google Apps authentication tatami.authentication.google.desc.1=This feature is for enterprise Google Apps users, who have their enterprise domain name managed by Google Apps. For more information on Google Apps, follow this link : tatami.authentication.google.desc.2=Whether or not you already have a Tatami account, you can sign in with your Google Apps account. tatami.authentication.google.desc.3=Your email will be provided by Google and your domain name will be used to make you join the corresponding private space. tatami.authentication.google.submit=Authentication with Google Apps tatami.authentication.cgv=Terms of Service tatami.remember.password.time=Remember me tatami.cg=Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. tatami.license.text=Licensed under the Apache License, Version 2.0 (the "License"); you may not use this application except in compliance with the License. You may obtain a copy of the License at tatami.license=Source Code License tatami.presentation=Welcome to Tatami, an Open Source enterprise social network tatami.presentation.moreinfo=See a detailed presentation of Tatami tatami.status.update=Update your status tatami.status.editor=Edit tatami.status.preview=Preview tatami.status.update.to=Send a status update tatami.status.private=Private message tatami.status.geoLocalization=Geolocalization tatami.status.update.drop.file=Drop your files here tatami.status.reply=Reply to this status tatami.status.reply.action=Reply tatami.user.favoritestatus=Favorites tatami.mentions=Mentions tatami.tags=Tags tatami.tag=Tag tatami.search=Search tatami.who.is.online.title=Who is online? tatami.find.title=Find a user tatami.find.username=Username tatami.find.action=Find tatami.follow.suggestions=Who to follow? tatami.follow.nobody=No new user to follow today. tatami.statistics=Statistics tatami.user.informations=Informations tatami.trends.title=Trends tatami.trends.user.title=User trends tatami.new.status=Update tatami.status.characters.left=Characters left: tatami.status.update.success=Your status has been updated! tatami.status.help.title=Help tatami.status.help=

    • You can use #tags by adding a # before a keyword
    • To mention a user, use his @username
    • To write rich content, you can use the MarkDown syntax, for example *text* to have a text in italic or **text** to have a text in bold.
    • If you write a link like http://www.ippon.fr, it will be automatically clickable
    • If you add a link to YouTube, Dailymotion or Vimeo, the online video will be automatically displayed
    • If you add a link to a Gist, its source code will be automatically displayed
    tatami.timeline=Timeline tatami.user.picture=Photo tatami.user.picture.button=Drop your photo here to update it tatami.user.picture.buttonIE=Select a photo to update your profile tatami.user.picture.buttonIE-ok=Photo updated! tatami.user.upload.buttonIE-ok=Select files to attach to your Tatam. tatami.user.upload.buttonIE-ko=An error occurred. Free some space in your account and try again. tatami.user.upload.choose=Choose a file tatami.user.email=E-mail tatami.user.firstName=First name tatami.user.lastName=Last name tatami.user.jobTitle=Job title tatami.user.phoneNumber=Phone number tatami.user.follow=Follow tatami.user.followed=Followed tatami.user.follows.you=follows you tatami.badge.status=Status tatami.badge.followed=Followed tatami.badge.followers=Followers tatami.user.undefined=User undefined. tatami.user.send=Send tatami.user.status=Status tatami.user=User tatami.user.file.name=Filename tatami.user.file.size=Size tatami.user.file.creation.date=Date tatami.user.file.preview=Preview tatami.user.file.delete.success=Your file has been deleted tatami.user.file.delete.error=Failed to delete your file tatami.menu.search=Search... tatami.menu.account=Account tatami.menu.profile=Profile tatami.menu.groups=Groups tatami.menu.password=Password tatami.menu.files=Files tatami.menu.directory=Users tatami.menu.tags=Tags tatami.menu.status.of.the.day=Statuses of the day tatami.menu.company.wall=Company wall tatami.menu.preferences=Preferences tatami.menu.groups.directory=Groups directory tatami.form.previous=Previous tatami.form.next=Next tatami.form.save=Save tatami.form.cancel=Cancel tatami.form.finish=Finish tatami.account.update.title=User profile tatami.account.update.legend=Update your profile tatami.user.update.success=Your profile has been successfully updated. tatami.user.update.error=An error has occurred! tatami.user.suppress=Delete the current user account tatami.user.suppress.confirmation=You are about to delete your account. Are you sure? tatami.user.password.legend=Update your password tatami.user.password.ldap=Your password is managed by an LDAP server, you cannot update it with Tatami. tatami.user.password.success=Your password has been successfully updated. tatami.user.old.password=Old password tatami.user.old.password.error=The old password is incorrect. tatami.user.new.password=New password tatami.user.new.password.confirmation=New password confirmation tatami.user.new.password.confirmation.error=The new password confirmation is incorrect tatami.preferences.notifications=Notifications tatami.preferences.notifications.email.mention=Get notified by e-mail when you are mentioned tatami.preferences.notifications.email.dailyDigest=Get a daily digest e-mail tatami.preferences.notifications.email.weeklyDigest=Get a weekly digest e-mail tatami.preferences.notifications.rss.timeline=Allow RSS feed publication of your timeline tatami.preferences.success=Your preferences have been saved tatami.user.profile.show=Show profile of tatami.user.profile.yourProfil=Your profile tatami.user.status.show=View tatami.user.status.details=Details tatami.user.status.reply=Reply tatami.user.status.share=Share tatami.user.status.share.success=Status successfully shared! tatami.user.status.favorite=Favorite tatami.user.status.delete=Delete tatami.user.status.announce=Announce tatami.user.status.replyto=In reply to tatami.user.status.shared.by=Shared by tatami.user.status.announced.by=Announced by tatami.user.shared.you=has shared your status tatami.user.followed.you=has followed you tatami.user.profile.edit=Edit my profile tatami.user.search.searchInStatus=Statuses with tatami.user.activate=Activate tatami.user.desactivate=Deactivate tatami.user.desactivate.msg=This user is deactivated tatami.user.desactivate.msg2= ( Deactivated user ) tatami.timeline.refresh=Refresh tatami.timeline.message=New message tatami.timeline.messages=New messages tatami.timeline.next=More tatami.timeline.shares=Shared by tatami.user.status.confirm.announce=Are you sure you want to send this status to everyone? tatami.user.status.confirm.delete=Are you sure you want to delete this status? tatami.group.name=Group tatami.group.counter=Members tatami.group.add=Create a new group tatami.group.add.title=Name of the group tatami.group.add.description=Description tatami.group.add.access=Access tatami.group.add.public=Public tatami.group.add.private=Private tatami.group.add.archived=Archived tatami.group.add.public.alert=Warning! If this group is public, everybody can access it tatami.group.add.success=Group created successfully tatami.group.archive=Do you want to archive this group? tatami.group.archive.true=Yes, this group should be archived tatami.group.archive.false=No, this group is still in use tatami.group.archive.alert=Archived groups are read-only tatami.group.list=Your groups tatami.group.edit.link=Manage tatami.group.edit.quit=Quit tatami.group.edit.success=Group updated successfully tatami.group.edit.list=Group members tatami.group.join.group=Join tatami.group.edit.details=Update group details tatami.group.edit.member.add.success=User successfully added tatami.group.edit.member.add.error=User could not be added! tatami.group.edit.member.remove.success=User successfully removed tatami.group.edit.member.add.no.user=You must enter a user name tatami.group.edit.member.add.wrong.user=This user name does not exist tatami.group.edit.member.add=Add a member tatami.group.edit.member.delete=Remove tatami.group.role=Role tatami.group.role.admin=Administrator tatami.group.role.member=Member tatami.group.select=Select a group on the "Groups" menu on the left tatami.group.members.list=Members list tatami.presentation.title=What is Tatami? tatami.presentation.row1.title=A private, enterprise social network tatami.presentation.row1.1=Update your status to inform your co-workers tatami.presentation.row1.2=Subscribe to other employees' time lines tatami.presentation.row1.3=Share important information to your followers tatami.presentation.row1.4=Discuss and reply to your colleagues tatami.presentation.row1.5=Put important information into favorites tatami.presentation.row1.6=Search useful information with our integrated search engine tatami.presentation.row1.7=Use hashtags to find related information tatami.presentation.row1.8=Go to your co-workers' profiles to see what they are working on tatami.presentation.row1.9=English and French versions available, adding other languages is easy tatami.presentation.row2.title=Works on all devices! tatami.presentation.row2.1=Dynamic Web application (HTML5) : nothing to install, excepted a modern browser! tatami.presentation.row2.2=Works on mobile devices, tablets, or standard computers : the application adapts itself automatically to your device's screen tatami.presentation.row2.3=Stay connected with your enterprise wherever you are tatami.presentation.row3.title=Easy installation and integration with your company's IT infrastructure tatami.presentation.row3.1=Standard Java application tatami.presentation.row3.2=Your data belongs to you, not to your SaaS vendor! tatami.presentation.row3.3=Integrates with your LDAP directory tatami.presentation.row3.4=Integrates with Google Apps tatami.presentation.row3.5=Fully Open Source, with a business-friendly Apache 2 license tatami.presentation.row3.6=Easy to extend or modify according to your needs tatami.presentation.row3.7=High performance (based on Apache Cassandra), even on small hardware tatami.presentation.row3.8=Join the project and submit patches on our Github page: tatami.presentation.row4.title=Also available in SaaS mode, fully managed by Ippon Technologies tatami.presentation.row4.1=If you do not want to install Tatami in your company, it's easy to use directly tatami.presentation.row4.2=Secured multi-enterprise mode: every company has its own private space tatami.presentation.row4.3=256 bits SSL cryptography: all data transfers are fully secured tatami.presentation.row5.title=Need more information on our product? tatami.presentation.row5.1=Our sales team is looking forward to hearing from you! Call us at +33 01 46 12 48 48 or e-mail us at tatami.account.users.myfriends=My friends tatami.account.users.recommended=Recommended tatami.account.users.all=Users tatami.account.groups.mygroups=My groups tatami.account.groups.recommended=Recommended tatami.account.tags.mytags=My tags tatami.account.tags.recommended=Recommended tatami.rss.timeline.title=Timeline for user {0} tatami.rss.timeline.description=Tatami timeline for user {0} tatami.preferences.notifications.rss.timeline.link=Link to your timeline RSS stream tatami.logo=Ippon Technologies Logo tatami.welcome.title=Welcome to Tatami tatami.welcome.description=Your timeline is empty! Do you need help to learn how to use Tatami? Please click on the button below to launch a presentation. tatami.welcome.launch=Launch presentation tatami.help=Help tatami.help.end=End tatami.help.next=Next » tatami.help.previous=« Prev tatami.help.home.presentation.title=Help tatami.help.home.presentation.content=Welcome to the online help!

    Follow the next steps for a tour of the main Tatami features.

    tatami.help.home.timeline.title=Timeline tatami.help.home.timeline.content=This is your timeline. It displays all messages \
      \
    • mentioning you or sent privately to you
    • \
    • sent by users you follow
    • \
    • sent by yourself
    • \
    • sent to group you are subscribed to
    \

    If it's empty, don't worry, it will get updated as soon as you start following other users!

    \

    When viewing a message, you can reply to it and mark it as favorite to find it easily later.

    \

    If a message had already got some replies, you can see them all by clicking on details: this makes it easier to follow conversation on Tatami.

    tatami.help.home.updatestatus.title=Sending messages tatami.help.home.updatestatus.content=Here is where you write messages you want to share \
    • all messages are public by default. They will be delivered to all users who follow you
    • \
    • when writing a message you should use #hashtags: this simply means adding a \'#\' at the beginning of important words that can be used to find your message
    • \
    • when mentioning, or replying to, other users, you should add a @ at the beginning of their name : they will be notified that you are talking to them
    • \
    tatami.help.home.groups.title=Groups tatami.help.home.groups.content=This is the list of groups you are a member of. \

    You can find, and subscribe to, public group in the Account/Groups page (top-right menu).

    \

    There are also private groups : for these you cannot subscribe : the owner of the group must add you as a member.

    tatami.help.home.follow-suggest.title=Suggested users tatami.help.home.follow-suggest.content=

    This is a list of users who share common interests with you and who you could follow.

    \

    If you are a new user, this list is probably empty: Tatami needs some time to learn who you are in order to suggest you relevant users.

    \

    And don't forget to use #hashtags in your messages, it makes everything easier!

    tatami.help.home.profileTrends.title=Trends tatami.help.home.profileTrends.content=

    This list represents the #hashtag that are currently the most often used on Tatami. Use this to discover what's going on and what are the hottest topics on Tatami !

    tatami.tatam.publish=Publish tatami.status.options=Options tatami.tatam.mandatory=Comment is mandatory ================================================ FILE: web/src/main/webapp/WEB-INF/messages/messages_fr.properties ================================================ tatami.copyright=Copyright 2012t tatami.ippon.technologies=Ippon Technologies tatami.github.fork=Forker Tatami sur Github tatami.github.issues=Signaler un bug tatami.ippon.website=Site Web d'Ippon Technologies tatami.ippon.blog=Blog d'Ippon Technologies tatami.ippon.twitter.follow=Suivre @ippontech sur Twitter tatami.title=Tatami tatami.profile=Profil tatami.home=Accueil tatami.menu.about=A Propos tatami.menu.presentation=Présentation tatami.menu.tos=Conditions générales de vente tatami.menu.language=Langue tatami.menu.language.en=English tatami.menu.language.fr=Français tatami.menu.license=Licence du code source tatami.search.placeholder=Rechercher tatami.search.button=Rechercher tatami.search.filter=Entrez votre recherche tatami.logout=Déconnexion tatami.login=E-mail tatami.username=Nom d'utilisateur tatami.password=Mot de Passe tatami.404=Page non trouvée. tatami.500=Une erreur s'est produite. tatami.file.not.found=Le fichier demandé n'existe pas. Il a probablement été supprimé par son propriétaire. tatami.form.success=Le formulaire a été correctement enregistré. tatami.form.error=Une erreur s'est produite lors de l'enregistrement du formulaire. tatami.login.modal.timeout.title=Attention tatami.login.modal.timeout.message=Vous avez été déconnecté. Vous devez vous identifier à nouveau pour accéder à Tatami tatami.login.modal.timeout.close=Fermer tatami.register.title=Vous n'avez pas (encore) de compte tatami.register.text.1=Pour créer votre compte Tatami, merci de renseigner votre adresse e-mail. tatami.register.text.2=Un message de confirmation sera envoyé à l'adresse e-mail que vous avez fournie. tatami.register.text.3=En fonction du nom de domaine de votre adresse e-mail, vous rejoindrez alors l'espace privé de votre entreprise. Par exemple, les utilisateurs avec une adresse de type email@ippon.fr rejoindront l'espace privé d'Ippon Technologies. tatami.register.text.4=Si vous êtes le premier employé de votre société à rejoindre Tatami, un espace privé pour votre société sera automatiquement créé. tatami.register=Enregistrement tatami.register.msg=Merci ! Un e-mail d'inscription vient de vous être envoyé. tatami.register.msg.error=Cette adresse e-mail est déjà utilisée par un utilisateur de Tatami. tatami.register.validation.title=Validation de votre adresse e-mail tatami.register.validation.error=Votre adresse e-mail n'a pas pu être validée. Merci d'utiliser à nouveau notre formulaire d'enregistrement. tatami.register.validation.ok=Votre adresse e-mail a été validée. Vous allez recevoir votre mot de passe par e-mail. tatami.register.home=Aller à la page d'accueil tatami.lost.password.title=Vous avez oublié votre mot de passe ? tatami.lost.password.button=Demander un nouveau mot de passe tatami.lost.password.msg=Un e-mail vous a été envoyé, vous expliquant comment réinitialiser votre mot de passe. tatami.lost.password.msg.error=Cette adresse e-mail n'est pas enregistrée dans Tatami. tatami.ldap.password.msg.error=Ce compte est géré via votre annuaire LDAP, vous ne pouvez pas réinitialiser votre mot de passe avec Tatami. tatami.authentification=Vous avez déjà un compte tatami.authentificate=Se Connecter tatami.authentification.error=Votre authentification a échoué ! Votre mot de passe est-il correct ? tatami.authentication.google.title=Authentification Google Apps tatami.authentication.google.desc.1=Cette fonctionnalité concerne les utilisateurs professionnels de Google Apps, et qui ont le nom de domaine de leur société associé à leur compte Google Apps. Pour plus d'informations sur Google Apps, suivez ce lien : tatami.authentication.google.desc.2=Que vous ayez déjà un compte Tatami ou non, vous pouvez vous connecter avec votre compte Google Apps. tatami.authentication.google.desc.3=Votre email sera demandé à Google et votre nom de domaine sera utilisé pour vous faire rejoindre l'espace privé correspondant. tatami.authentication.google.submit=Connexion avec Google Apps tatami.authentication.cgv=Conditions générales de vente tatami.remember.password.time=Se souvenir de moi tatami.cg=Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. tatami.license.text=Licensed under the Apache License, Version 2.0 (the "License"); you may not use this application except in compliance with the License. You may obtain a copy of the License at tatami.license=Licence du code source tatami.presentation=Bienvenue sur Tatami, un réseau social d'entreprise Open Source tatami.presentation.moreinfo=Voir une présentation détaillée de Tatami tatami.status.update=Mettre à jour votre statut tatami.status.editor=Edition tatami.status.preview=Prévisualisation tatami.status.update.to=Envoyer une mise à jour du statut tatami.status.private=Message privé tatami.status.geoLocalization=Géolocaliser tatami.status.update.drop.file=Déposez vos fichiers ici tatami.status.reply=Répondre à ce statut tatami.status.reply.action=Répondre tatami.user.favoritestatus=Favoris tatami.mentions=Mentions tatami.tags=Tags tatami.tag=Tag tatami.search=Recherche tatami.who.is.online.title=Qui est en ligne ? tatami.find.title=Chercher un utilisateur tatami.find.username=Identifiant utilisateur tatami.find.action=Chercher tatami.follow.suggestions=Qui suivre ? tatami.follow.nobody=Personne à suivre aujourd'hui. tatami.statistics=Statistiques tatami.user.informations=Informations tatami.trends.title=Tendances tatami.trends.user.title=Tendances pour l'utilisateur tatami.new.status=Nouveau tatami.status.characters.left=Caractères restants : tatami.status.update.success=Votre statut a été mis à jour ! tatami.status.help.title=Aide tatami.status.help=
    • Vous pouvez utiliser des #tags en ajoutant un # devant un mot-clef
    • Pour mentionner un utilisateur, utilisez son @nom_utilisateur
    • Pour enrichir votre texte, vous pouvez utiliser la syntaxe MarkDown, par exemple *texte* pour écrire en italique ou **texte** pour écrire en gras.
    • Si vous écrivez un lien de type http://www.ippon.fr, il sera automatiquement cliquable
    • Si vous ajoutez un lien provenant de YouTube, Dailymotion ou Vimeo, la vidéo en ligne sera automatiquement insérée
    • Si vous ajoutez un lien pointant vers un Gist, le code source sera automatiquement affiché
    tatami.timeline=Actualité tatami.user.picture=Photo tatami.user.picture.button=Déposez votre photo dans le cadre pour la mettre à jour tatami.user.picture.buttonIE=Sélectionnez une photo pour mettre à jour votre profil. tatami.user.picture.buttonIE-ok=Photo mise à jour correctement. tatami.user.upload.buttonIE-ok=Sélectionnez des fichiers à ajouter à votre message. tatami.user.upload.buttonIE-ko=Une erreur s'est produite, pensez à supprimer des anciens fichiers dans votre compte. tatami.user.upload.choose=Choisir un fichier tatami.user.email=E-mail tatami.user.firstName=Prénom tatami.user.lastName=Nom tatami.user.jobTitle=Fonction tatami.user.phoneNumber=Numéro de téléphone tatami.user.follow=Suivre tatami.user.followed=Abonné tatami.user.follows.you=vous suit tatami.badge.status=Statuts tatami.badge.followed=Abonnements tatami.badge.followers=Abonnés tatami.user.undefined=Utilisateur inconnu. tatami.user.send=Envoi tatami.user.status=Statut tatami.user=Utilisateurs tatami.user.file.name=Nom du fichier tatami.user.file.size=Taille tatami.user.file.creation.date=Date tatami.user.file.preview=Aperçu tatami.user.file.delete.success=Votre fichier a été supprimé tatami.user.file.delete.error=Votre fichier n'a pas été supprimé tatami.menu.search=Recherche... tatami.menu.account=Compte tatami.menu.profile=Profil tatami.menu.groups=Groupes tatami.menu.password=Mot de passe tatami.menu.files=Fichiers tatami.menu.directory=Utilisateurs tatami.menu.tags=Tags tatami.menu.status.of.the.day=Messages du jour tatami.menu.company.wall=Mur de l'entreprise tatami.menu.preferences=Préférences tatami.menu.groups.directory=Annuaire des groupes tatami.form.previous=Précédent tatami.form.next=Suivant tatami.form.save=Sauvegarder tatami.form.cancel=Annuler tatami.form.finish=Fin tatami.account.update.title=Profil utilisateur tatami.account.update.legend=Mise à jour du profil tatami.user.update.success=Votre profil a été mis à jour. tatami.user.update.error=Une erreur s'est produite ! tatami.user.suppress=Destruction du compte utilisateur tatami.user.suppress.confirmation=Etes-vous sûr de vouloir détruire votre compte ? tatami.user.password.legend=Mise à jour de votre mot de passe tatami.user.password.ldap=Votre mot de passe est géré par LDAP, vous ne pouvez pas le modifier via Tatami. tatami.user.password.success=Votre mot de passe a été mis à jour. tatami.user.old.password=Ancien mot de passe tatami.user.old.password.error=Ancien mot de passe incorrect tatami.user.new.password=Nouveau mot de passe tatami.user.new.password.confirmation.error=La confirmation du nouveau mot de passe est incorrecte tatami.user.new.password.confirmation=Confirmation du mot de passe tatami.preferences.notifications=Notifications tatami.preferences.notifications.email.mention=Recevoir un e-mail lorsqu'on vous mentionne tatami.preferences.notifications.email.dailyDigest=Recevoir un résumé quotidien de votre actualité tatami.preferences.notifications.email.weeklyDigest=Recevoir un résumé hebdomadaire de votre actualité tatami.preferences.notifications.rss.timeline=Autoriser la publication d'un flux RSS de votre actualité tatami.preferences.success=Vos préférences ont été sauvegardées tatami.user.profile.show=Profil de tatami.user.profile.yourProfil=Votre profil tatami.user.status.show=Voir tatami.user.status.details=Détails tatami.user.status.reply=Répondre tatami.user.status.share=Partager tatami.user.status.share.success=Message partagé ! tatami.user.status.favorite=Favori tatami.user.status.delete=Supprimer tatami.user.status.announce=Annoncer tatami.user.status.replyto=En réponse à tatami.user.status.shared.by=Partagé par tatami.user.status.announced.by=Annoncé par tatami.user.shared.you=a partagé votre message tatami.user.followed.you=vous a suivi tatami.user.profile.edit=Editer mon profil tatami.user.search.searchInStatus=Messages contenant tatami.user.activate=Activer tatami.user.desactivate=Désactiver tatami.user.desactivate.msg=Cet utilisateur est désactivé tatami.user.desactivate.msg2= ( Utilisateur désactivé ) tatami.timeline.refresh=Rafraîchir tatami.timeline.message=Nouveau message tatami.timeline.messages=Nouveaux messages tatami.timeline.next=Plus tatami.timeline.shares=Partagé par tatami.user.status.confirm.announce=Etes-vous certain de vouloir envoyer ce message à tout le monde ? tatami.user.status.confirm.delete=Etes-vous certain de vouloir supprimer ce message ? tatami.group.name=Groupe tatami.group.counter=Membres tatami.group.add=Créer un nouveau groupe tatami.group.add.title=Nom du groupe tatami.group.add.description=Description tatami.group.add.access=Accès tatami.group.add.public=Public tatami.group.add.private=Privé tatami.group.add.archived=Archivé tatami.group.add.public.alert=Attention ! Si ce groupe est public, tout le monde y aura accès tatami.group.add.success=Groupe créé avec succès tatami.group.archive=Voulez-vous archiver ce groupe ? tatami.group.archive.true=Oui, ce groupe doit être archivé tatami.group.archive.false=Non, ce groupe est toujours utilisé tatami.group.archive.alert=Les groupes archivés sont en lecture seule tatami.group.list=Vos groupes tatami.group.edit.link=Gestion tatami.group.edit.quit=Quitter tatami.group.join.group=Rejoindre tatami.group.edit.success=Groupe mis à jour avec succès tatami.group.edit.details=Modifier les détails du groupe tatami.group.edit.list=Liste des membres tatami.group.edit.member.add.success=Utilisateur ajouté avec succès tatami.group.edit.member.add.error=L'utilisateur n'a pas pu être ajouté ! tatami.group.edit.member.remove.success=L'utilisateur ne fait plus partie du groupe tatami.group.edit.member.add.no.user=Vous devez entrer le nom d'un utilisateur tatami.group.edit.member.add.wrong.user=Cet utilisateur n'existe pas tatami.group.edit.member.add=Ajouter un membre tatami.group.edit.member.delete=Enlever tatami.group.role=Role tatami.group.role.admin=Administrateur tatami.group.role.member=Membre tatami.group.select=Sélectionnez un groupe dans le menu "Groupes" à gauche tatami.group.members.list=Liste des membres tatami.presentation.title=Qu'est-ce que Tatami ? tatami.presentation.row1.title=Un réseau social d'entreprise privé tatami.presentation.row1.1=Mettre à jour votre statut pour que vos collègues soient informés tatami.presentation.row1.2=S'abonner aux fils d'information de vos collègues tatami.presentation.row1.3=Renvoyer les informations importantes aux personnes qui vous suivent tatami.presentation.row1.4=Discuter et répondre à vos collègues tatami.presentation.row1.5=Mettre en favori les informations essentielles tatami.presentation.row1.6=Rechercher l'information utile grâce à notre moteur de recherche intégré tatami.presentation.row1.7=Utiliser des hashtags pour découvrir des informations connexes tatami.presentation.row1.8=Aller sur les profils de vos collègues pour voir ce sur quoi ils travaillent tatami.presentation.row1.9=Versions Française et Anglaise disponibles, de nouvelles langues sont simples à ajouter tatami.presentation.row2.title=Fonctionne partout ! tatami.presentation.row2.1=Application Web entièrement dynamique (HTML5) : rien à installer, à part un navigateur Web moderne ! tatami.presentation.row2.2=Fonctionne sur tout support : mobile, tablette, ou PC "classique", l'application s'adapte automatiquement à l'écran tatami.presentation.row2.3=Restez connecté à votre entreprise où que vous soyez tatami.presentation.row3.title=S'installe facilement et s'intègre sans problème dans votre SI tatami.presentation.row3.1=Application Java standard tatami.presentation.row3.2=Contrairement aux solutions SaaS, vos données vous appartiennent entièrement ! tatami.presentation.row3.3=S'intègre avec votre serveur LDAP tatami.presentation.row3.4=S'intègre avec Google Apps tatami.presentation.row3.5=Entièrement Open Source, sous licence Apache 2 (dite "business friendly") tatami.presentation.row3.6=Simple à étendre ou à modifier en fonction de vos besoins tatami.presentation.row3.7=Peu gourmande en ressources et très performante (basée sur Apache Cassandra) tatami.presentation.row3.8=Participez au développement et proposez des corrections via notre page Github : tatami.presentation.row4.title=Disponible également en version hébergée, entièrement gérée par Ippon Technologies tatami.presentation.row4.1=Si vous ne voulez pas installer Tatami chez vous, rien de plus simple que d'utiliser directement tatami.presentation.row4.2=Mode multi-entreprise sécurisé : chaque entreprise dispose de son propre espace tatami.presentation.row4.3=Chiffrage SSL fort (256 bits) en standard : tous les transferts de données sont sécurisés tatami.presentation.row5.title=Une question ? tatami.presentation.row5.1=Notre équipe commerciale reste à votre écoute - Téléphonez-nous au 01 46 12 48 48 ou envoyez-nous un e-mail à tatami.account.users.myfriends=Mes amis tatami.account.users.recommended=Recommandés tatami.account.users.all=Utilisateurs tatami.account.groups.mygroups=Mes groupes tatami.account.groups.recommended=Recommandés tatami.account.tags.mytags=Mes tags tatami.account.tags.recommended=Recommandés tatami.rss.timeline.title=Actualité de {0} tatami.rss.timeline.description=Actualité Tatami de {0} tatami.preferences.notifications.rss.timeline.link=Lien vers le flux RSS de votre actualité tatami.logo=Ippon Technologies tatami.welcome.title=Bienvenue sur Tatami tatami.welcome.description=Votre actualité est vide ! Avez-vous besoin d'aide pour apprendre à utiliser Tatami ? Si oui, cliquez sur le bouton pour lancer la présentation. tatami.welcome.launch=Lancer la présentation tatami.help=Aide tatami.help.end=Terminer tatami.help.next=Suivant » tatami.help.previous=« Précédent tatami.help.home.presentation.title=Aide tatami.help.home.presentation.content=Bienvenue dans l'aide !

    Nous allons vous guider à travers les principales fonctionnalités de Tatami.

    tatami.help.home.timeline.title=Actualité tatami.help.home.timeline.content=Cette zone représente votre actualité. Elle contient tous les messages\
      \
    • que vous avez postés vous-même,
    • \
    • dans lesquels vous êtes cité, ou qui vous ont été envoyés en privé,
    • \
    • postés par les utilisateurs que vous suivez,
    • \
    • postés dans un groupe dont vous faites partie.
    \

    Ne vous inquiétez pas si cette liste est vide, elle se remplira dès que vous aurez commencé à suivre d'autres utilisateurs !

    \

    Pour chacun des messages de cette liste, vous pouvez répondre et le marquer comme favori, afin de le retouver facilement plus tard.

    \

    Si un message a déjà eu des réponses, vous pouvez toutes les afficher en cliquant sur \'Détails' : cela permet de suivre facilement des conversations sur Tatami.

    tatami.help.home.updatestatus.title=Poster des messages tatami.help.home.updatestatus.content=C'est ici que vous rédigez les messages que vous voulez poster \
    • tous les messages sont publics par défaut. Ils seront envoyés à tous les utilisateurs qui vous suivent,
    • \
    • quand vous rédigez un message, il est conseillé d'utiliser des #hashtags : il s'agit simplement d'ajouter le caractère \'#\' au début des mots importants qui pourront être utilisés pour retrouver votre message,
    • \
    • lorsque vous mentionnez un utilisateur ou que vous répondez à quelqu'un, il faut faire précéder son nom du symbol \'@\' : il sera ainsi notifié que quelqu'un lui parle.
    • \
    tatami.help.home.groups.title=Groupes tatami.help.home.groups.content=Voici la liste des groupes dont vous faites partie. \

    Vous pouvez trouver la liste des groupes publics auxquels vous inscrire dans la page \'Compte/Groupe\' (dans le menu en haut à droite).

    \

    Il existe aussi des groupes privés : vous ne pouvez pas vous y inscrire vous même, seul le propriétaire d'un groupe privé peut vous déclarer comme membre de ce groupe.

    \ tatami.help.home.follow-suggest.title=Utilisateurs suggérés tatami.help.home.follow-suggest.content=

    Cette liste vous propose des utilisateurs qui partagent les mêmes centres d'intérêts que vous et que vous pourriez éventuellement suivre.

    \

    Si vous êtes un nouvel utilisateur, cette liste est probablement vide : Tatami a besoin d'un peu de temps pour apprendre qui vous êtes et vous proposer des utilisateurs similaires.

    \

    Et surtout, n'oubliez pas de poster des messages et d'utiliser des #hashtags, cela permet à Tatami de vous proposer plus rapidement des utilisateurs à suivre !

    tatami.help.home.profileTrends.title=Tendances tatami.help.home.profileTrends.content=

    Cette liste représente les #hashtag les plus utilisés ces derniers jours sur Tatami. Utilisez-la pour découvrir les dernières tendances et les sujets que vous ne connaissez pas !

    tatami.tatam.publish=Publier tatami.status.options=Options tatami.tatam.mandatory=Commentaire obligatoire ================================================ FILE: web/src/main/webapp/WEB-INF/pages/errors/404.jsp ================================================ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

    ================================================ FILE: web/src/main/webapp/WEB-INF/pages/errors/500.jsp ================================================ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

    ================================================ FILE: web/src/main/webapp/WEB-INF/pages/errors/file_not_found.jsp ================================================ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

    ================================================ FILE: web/src/main/webapp/WEB-INF/pages/home.jsp ================================================ <%@ page language="java" pageEncoding="UTF-8" contentType="text/html; charset=utf-8" %> If you are not redirected automatically, click here. ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/footer.jsp ================================================ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/header.jsp ================================================ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page import="Constants" %> <% String version = Constants.VERSION; request.setAttribute("version", version); String googleAnalyticsKey = Constants.GOOGLE_ANALYTICS_KEY; request.setAttribute("googleAnalyticsKey", googleAnalyticsKey); %> <fmt:message key="tatami.title"/> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/help-home.jsp ================================================ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/navigation-admin.jsp ================================================ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/template-search-engine.jsp ================================================ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/templates-admin.jsp ================================================ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/templates.jsp ================================================ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/topavatar.jsp ================================================ <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/includes/topmenu.jsp ================================================ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ================================================ FILE: web/src/main/webapp/WEB-INF/pages/login.jsp ================================================ <%@ page language="java" pageEncoding="UTF-8" contentType="text/html; charset=utf-8" %> If you are not redirected automatically, click here. ================================================ FILE: web/src/main/webapp/WEB-INF/web.xml ================================================ Tatami fr.ippon.tatami.web.init.WebConfigurer GzipFilter fr.ippon.tatami.web.filter.TatamiGzipFilter CharacterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceEncoding true CharacterEncodingFilter /* GzipFilter /* 500 /tatami/errors/500 404 /tatami/errors/404 ================================================ FILE: web/src/main/webapp/app/TatamiApp.js ================================================ var TatamiApp = angular.module('TatamiApp', [ 'TopMenuModule', 'LoginModule', 'HomeModule', 'AccountModule', 'AboutModule', 'AdminModule', 'FooterModule', 'ngResource', 'ngTouch', 'ngCookies', 'pascalprecht.translate', 'ui.router', 'ui.bootstrap', 'mentio', 'LocalStorageModule', 'bm.bsTour', 'TatamiApp.services' ]); TatamiApp.run(['$rootScope', '$state', function($rootScope, $state) { $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState) { if($state.includes('tatami.home') && toState !== fromState) { document.body.scrollTop = document.documentElement.scrollTop = 0; } }); }]); TatamiApp.run(['$rootScope', '$state', '$stateParams', 'AuthenticationService', 'UserSession', function($rootScope, $state, $stateParams, AuthenticationService, UserSession) { // Make state information available to $rootScope, and thus $scope in our controllers $rootScope.$state = $state; $rootScope.$stateParams = $stateParams; // When the app is started, determine if the user is authenticated, if so, send them to home timeline UserSession.authenticate().then(function(result) { if(result !== null) { // We aren't logged in, clear the old session, and send the user to the login page if(result.action === null) { if(angular.isDefined($state.current.data) && !$state.current.data.public) { UserSession.clearSession(); $state.go('tatami.login.main'); } // If we are trying to access a state that is public, allow user if(angular.isDefined($rootScope.destinationState)) { $state.go($rootScope.destinationState, $rootScope.destinationParams); } else { // The destination state is undefined -- This is the first time accessing the site UserSession.clearSession(); $state.go('tatami.login.main'); } } // We are logged in, but the session hasn't been set, so we set it if(angular.isDefined(result.username)) { if(!UserSession.isAuthenticated()) { UserSession.setLoginState(true); } // If there is no destination state, send user to timeline, since they are logged in if(!angular.isDefined($rootScope.destinationState)) { $state.go('tatami.home.home.timeline'); } } } else { $state.go('tatami.login.main'); } }); $rootScope.$on('$stateChangeError', function(event) { event.preventDefault(); $state.transitionTo('tatami.login.main', null, { location: false }); }); $rootScope.$on('$stateChangeStart', function(event, toState, toStateParams) { $rootScope.destinationState = toState; $rootScope.destinationParams = toStateParams; // If the user is logged in, we allow them to go where they intend to if(UserSession.isAuthenticated()) { return; } if(toState.data && toState.data.public) { return; } // The user is not logged in, and trying to access a state that requires them to be logged in // Stash the state they tried to access $rootScope.returnToState = toState; $rootScope.returnToParams = toStateParams; // Go to login page event.preventDefault(); $state.go('tatami.login.main'); }); }]); TatamiApp.config(['$resourceProvider', '$locationProvider', '$urlRouterProvider', '$stateProvider', function($resourceProvider, $locationProvider, $urlRouterProvider, $stateProvider) { // Don't strip trailing slashes from REST URLs $resourceProvider.defaults.stripTrailingSlashes = false; $stateProvider .state('tatami', { url: '', abstract: true, views: { 'topMenu@': { templateUrl: 'app/shared/topMenu/TopMenuView.min.html', controller: 'TopMenuController' }, '': { templateUrl: 'index.html' }, 'footer@': { templateUrl: 'app/shared/footer/FooterView.min.html', controller: 'FooterController' } }, resolve: { authorize: ['AuthenticationService', function(AuthenticationService) { return AuthenticationService.authenticate(); }] }, data: { public: false, roles: ["ROLE_USER"] } }) .state('tatami.pageNotFound', { templateUrl: 'app/shared/error/404View.min.html', data: { public: true } }) .state('tatami.accessdenied', { templateUrl: 'app/shared/error/500View.min.html', data: { public: true } }); }]); ================================================ FILE: web/src/main/webapp/app/components/about/AboutModule.js ================================================ var AboutModule = angular.module('AboutModule', []); AboutModule.config(['$stateProvider', function($stateProvider) { $stateProvider .state('tatami.about',{ url: '/about', abstract: true, templateUrl: 'app/components/about/AboutView.html' }) .state('tatami.about.presentation', { url: '/presentation', views: { 'aboutBody': { templateUrl: 'app/components/about/presentation/PresentationView.min.html' } }, data: { public: true } }) .state('tatami.about.tos', { url: '/tos', views: { 'aboutBody': { templateUrl: 'app/components/about/tos/ToSView.min.html' } }, data: { public: true } }) .state('tatami.about.license', { url: '/license', views: { 'aboutBody': { templateUrl: 'app/components/about/license/LicenseView.min.html', controller: 'LicenseController' } }, data: { public: true } }); }]); ================================================ FILE: web/src/main/webapp/app/components/about/AboutView.html ================================================
    ================================================ FILE: web/src/main/webapp/app/components/about/license/LicenseController.js ================================================ AboutModule.controller('LicenseController', ['$scope', function($scope) { $scope.time=" 2012-"+new Date().getFullYear()+" "; $("#endYear").html($scope.time); } ]); ================================================ FILE: web/src/main/webapp/app/components/about/license/LicenseView.html ================================================

    © 2012 Ippon Technologies

    Licensed under the Apache License, Version 2.0 (the "License"); you may not use this application except in compliance with the License. You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

    ================================================ FILE: web/src/main/webapp/app/components/about/presentation/PresentationView.html ================================================













    ================================================ FILE: web/src/main/webapp/app/components/about/tos/ToSView.html ================================================

    1 - General

    1. By using any of the Ippon Technologies products, software, or services available at *.ippon.fr (collectively referred to as "Services") provided by Ippon Techonologies ("Ippon"), you agree to be bound by the following terms and conditions ("Terms of Service").
    2. Ippon reserves the right to update and change the Terms of Service at any time without notice. You can review the most current version of the Terms of Service at: https://tatami.ippon.fr/tatami/tos
    3. The failure of Ippon to exercise or enforce any right or provision of the Terms of Service shall not constitute a waiver of such right or provision.
    4. You agree that Ippon owns all legal rights in the Service. You must not remove, modify or obscure any legal notices. You are not entitled to use Ippon's trademarks (especially "Ippon Technologies"), trade names, brands, domain names, or other distinctive brand features.
    5. Violation of any of the terms below can result in the termination of your account or access to the Services. Violation of any of the terms will also terminate your usage rights of the Services.
    6. If a provision of this agreement is or becomes illegal, invalid or unenforceable in any jurisdiction, that shall not affect: (i) the validity or enforceability in that jurisdiction of any other provision of this agreement; or (ii) the validity or enforceability in other jurisdictions of that or any other provision of this agreement.
    7. These Terms of Service constitute the entire agreement between you and Ippon und replace all previous agreements under this title. There are no verbal subsidiary agreements.
    8. These Terms of Service, and your relationship with Ippon under these Terms of Service, shall be governed by the laws of France. You and Ippon agree to submit to the exclusive jurisdiction of the courts located in France to resolve any legal matter arising from the Terms of Service.

    2 - Provision of the Services by Ippon Technologies

    1. Your use of the Services is at your sole risk. The Services are provided on an "as is" and "as available" basis.
    2. Ippon does not warrant that (i) the Services will meet your requirements or expectations, (ii) the Services will be delivered uninterrupted, timely, secure, or error-free, (iii) the results that may be obtained from the use of the Services will be accurate or reliable, (iv) any errors in the Services will be corrected.
    3. You understand and agree that Ippon shall not be liable for any direct, indirect, incidental, special, consequential or exemplary damages, including but not limited to any loss of profit, loss of goodwill, loss of business reputation, loss of data, cost of procurement of substitute goods or Services, or other intangible loss, resulting from: (i) the use or the inability to use the Services; (ii) any changes which Ippon may make to the Services, or any permanent or temporary cessation in the provision of the Services; (iii) unauthorized access to or alteration of your transmissions or data; (iv) the deletion of, corruption of, or failure to store, any content and other communications data maintained or transmitted by or through the use of the Services; (v) or any other matter relating to the Services.
    4. Technical support is provided on a best-effort basis and only by e-mail.
    5. You understand that Ippon can use third parties (e.g., hosting partners) to provide the necessary resources (e.g., hardware, software, networking, storage, etc.) to run the Services.
    6. Ippon may stop, remove, modify, or add (permanently or temporarily) Services (or features within Services) at Ippon's sole discretion. Any new, changed, or removed features are subject to the Terms of Service. Continued use of the Services after any such changes constitute your consent to the changes.

    3 - Use of the Services by You

    1. You agree to use the Services only for purposes that are permitted by (i) the Terms of Service and (ii) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions.
    2. For certain services, Ippon may offer or require you to register for an account. You ensure that all registration information you provide will always be valid, correct, and up to date. You are responsible for maintaining the security of your account and password. Accounts registered by bots or other automated methods are not permitted.
    3. For certain services, Ippon may offer to upload your personal profile photo. You are responsible to not upload an abusive photo, to not infringe on any copyrights or trademarks, and to not use a design which other Ippon users may deem offensive.
    4. For certain services, Ippon may offer to define a personal URL (web address). You understand that Ippon can rename or reject your personal URL, e.g., if you intentionally or unintentionally interfere with a trademark owner or because your personal URL is deemed offensive.
    5. You agree that you will not impersonate another person.
    6. You agree that you will not engage in any activity that interferes with or disrupts the Services (or the servers and networks which are connected to the Services).
    7. You agree that you will not reproduce, duplicate, copy, sell, trade or resell the Services for any purpose without the express written permission by Ippon.
    8. You agree that you are solely responsible for (and that Ippon has no responsibility to you or to any third party for) any breach of your obligations under the Terms of Service and for the consequences (including any loss or damage which Ippon may suffer) of any such breach.
    9. Ippon reserves the right (but has no obligation) to pre-screen, review, flag, filter, modify, refuse or remove any or all content or account from any of the Services at Ippon's sole discretion. You understand that by using the Services you may be exposed to content that you may find offensive, indecent or objectionable and that, in this respect, you use the Services at your own risk.

    4 - Advertisements

    1. Some of the Services are supported by advertising revenue and may display advertisements and promotions. These advertisements may be targeted to the content of information stored on or made available to the Services.
    2. The manner, mode and extent of advertising on the Services are subject to change without specific notice to you.
    3. You agree that Ippon may place such advertising on the Services.
    4. You must not block such advertising.

    5 - API Terms

    1. Any use of the application programming interface (API) is bound by the terms of this agreement plus the following specific terms:
    2. Abuse or excessively frequent requests via the API may result in the temporary or permanent suspension of access to the API. Ippon determines abuse or excessive usage of the API at its sole discretion.
    3. Ippon reserves the right at any time to modify or discontinue, temporarily or permanently, your access to the API (or any part thereof) with or without notice. Ippon especially reserves the right to limit the number of requests via the API to a certain upper limit per time interval.
    4. Your use of the API is free of charge up to a certain extent. Larger amounts of API transactions will be charged.
    5. Ippon reserves the right to change the API's pricing model at any time. In such a case, you will of course be free to discontinue using the API.
    6. As a user of the API, you may, within your product, indicate that your product utilizes the Ippon API. At other places or in other contexts, however, you may not use either the Ippon Technologies brand name or the Ippon Technologies logo. In particular, you may not use either the Ippon Technologies brand or the Ippon Technologies logo in the name of your product.

    Effective: August 7th, 2012

    ================================================ FILE: web/src/main/webapp/app/components/account/AccountController.js ================================================ AccountModule.controller('AccountController', ['$scope', '$location', 'profileInfo', function($scope, $location, profileInfo) { $scope.profile = profileInfo; }]); ================================================ FILE: web/src/main/webapp/app/components/account/AccountModule.js ================================================ var AccountModule = angular.module('AccountModule', [ 'ProfileModule', 'PreferencesModule', 'PasswordModule', 'FilesModule', 'UsersModule', 'GroupsModule', 'TagsModule', 'TopPostersModule', 'ngToast' ]); AccountModule.config(['$stateProvider', '$urlRouterProvider', function($stateProvider) { $stateProvider .state('tatami.account',{ url: '/account', abstract: true, templateUrl: 'app/components/account/AccountView.min.html', resolve: { profileInfo: ['ProfileService', function(ProfileService) { return ProfileService.get().$promise; }], userRoles: ['$http', function($http) { return $http({ method: 'GET', url: '/tatami/rest/account/admin' }).then(function(result) { return result.data; }); }] }, controller: 'AccountController' }) .state('tatami.account.profile', { url: '/profile', templateUrl: 'app/components/account/profile/ProfileView.min.html', resolve: { userLogin: ['UserService', 'profileInfo', function(UserService, profileInfo) { return UserService.get({ username: profileInfo.username }).$promise; }] }, controller: 'ProfileController' }) .state('tatami.account.preferences', { url: '/preferences', templateUrl: 'app/components/account/preferences/PreferencesView.min.html', resolve: { prefs: ['PreferencesService', function(PreferencesService) { return PreferencesService.get().$promise; }] }, controller: 'PreferencesController' }) .state('tatami.account.password', { url: '/password', templateUrl: 'app/components/account/password/PasswordView.min.html', controller: 'PasswordController' }) .state('tatami.account.files', { url: '/files', templateUrl: 'app/components/account/files/FilesView.min.html', resolve: { FilesService: 'FilesService', attachmentQuota: function(FilesService) { return FilesService.getQuota().$promise; }, fileList: function(FilesService) { return FilesService.query().$promise; } }, controller: 'FilesController' }) .state('tatami.account.users', { url: '/users', templateUrl: 'app/components/account/FormView.min.html', controller: 'FormController' }) .state('tatami.account.users.following', { url: '/following', templateUrl: 'app/components/account/users/UsersView.html', resolve: { usersList: ['profileInfo', 'UserService', function(profileInfo, UserService) { return UserService.getFollowing({ username: profileInfo.username }).$promise; }] }, controller: 'UsersController' }) .state('tatami.account.users.recommended', { url: '/recommended', templateUrl: 'app/components/account/users/UsersView.min.html', resolve: { usersList: ['UserService', function(UserService) { UserService.getSuggestions().$promise; }] }, controller: 'UsersController' }) .state('tatami.account.users.search', { url: '/search/:q', templateUrl: 'app/components/account/users/UsersView.min.html', resolve: { usersList: ['SearchService', '$stateParams', function(SearchService, $stateParams) { return SearchService.query({ term: 'users', q: $stateParams.q }).$promise; }] }, controller: 'UsersController' }) .state('tatami.account.groups', { url: '/groups', templateUrl: 'app/components/account/FormView.min.html', controller: 'FormController' }) .state('tatami.account.groups.main', { url: '', templateUrl: 'app/components/account/groups/GroupsView.min.html', controller: 'GroupController' }) .state('tatami.account.groups.main.top', { url: '', views: { 'create@tatami.account.groups.main': { templateUrl: 'app/components/account/groups/creation/GroupsCreateView.min.html', controller: 'GroupsCreateController' } } }) .state('tatami.account.groups.main.top.list', { url: '', views: { 'list@tatami.account.groups.main': { templateUrl: 'app/components/account/groups/list/GroupsListView.min.html', resolve: { userGroups: ['GroupService', function(GroupService) { return GroupService.query().$promise; }] }, controller: 'GroupsController' } } }) .state('tatami.account.groups.main.top.recommended', { url: '/recommended', views: { 'list@tatami.account.groups.main': { templateUrl: 'app/components/account/groups/list/GroupsListView.min.html', resolve: { userGroups: ['GroupService', function(GroupService) { return GroupService.getRecommendations().$promise; }] }, controller: 'GroupsController' } } }) .state('tatami.account.groups.main.top.search', { url: '/search/:q', views: { 'list@tatami.account.groups.main': { templateUrl: 'app/components/account/groups/list/GroupsListView.min.html', resolve: { userGroups: ['SearchService', '$stateParams', function(SearchService, $stateParams) { return SearchService.query({ term: 'groups', q: $stateParams.q }).$promise; }] }, controller: 'GroupsController' } } }) .state('tatami.account.groups.manage', { url:'/:groupId', templateUrl: 'app/components/account/groups/manage/GroupsManageView.min.html', resolve: { group: ['GroupService', '$stateParams', function(GroupService, $stateParams) { return GroupService.get({ groupId: $stateParams.groupId }).$promise; }], members: ['GroupService', '$stateParams', function(GroupService, $stateParams) { return GroupService.getMembers({ groupId: $stateParams.groupId }).$promise; }] }, controller:'GroupsManageController' }) .state('tatami.account.tags', { url:'/tags', templateUrl: 'app/components/account/FormView.min.html', controller: 'FormController' }) .state('tatami.account.tags.following', { url: '/following', templateUrl: 'app/components/account/tags/TagsView.min.html', resolve: { tagList: ['TagService', function(TagService) { return TagService.query().$promise; }] }, controller: 'TagsController' }) .state('tatami.account.tags.trends', { url: '/trends', templateUrl: 'app/components/account/tags/TagsView.min.html', resolve: { tagList: ['TagService', function(TagService) { return TagService.getPopular().$promise; }] }, controller: 'TagsController' }) .state('tatami.account.tags.search', { url: '/search/:q', templateUrl: 'app/components/account/tags/TagsView.min.html', resolve: { tagList: ['SearchService', '$stateParams', function(SearchService, $stateParams) { if($stateParams.q.length === 0) { return {}; } else { return SearchService.query({ term: 'tags', q: $stateParams.q }).$promise; } }] }, controller: 'TagsController' }) .state('tatami.account.topPosters', { url: '/top', templateUrl: 'app/components/account/topPosters/TopPostersView.min.html', resolve: { topPosters: ['TopPostersService', function(TopPostersService) { // Get the {username, count} pairs for all the top posters return TopPostersService.query().$promise; }], users: ['topPosters', 'UserService', '$q', function(topPosters, UserService, $q) { // For all the {user, count} pairs, find the user data var temp = []; for(var i = 0; i < topPosters.length; ++i) { // Store the result as a promise temp.push(UserService.get({ username: topPosters[i].username }).$promise); temp[i].statusCount = topPosters[i].statusCount; } // return all promises return $q.all(temp); }], userData: ['topPosters', 'users', function(topPosters, users) { var statusCounts = []; // We want to associate the {username, count} pairs from the original to the user data, index based // on username. for(var i = 0; i < topPosters.length; ++i) { statusCounts[topPosters[i].username] = topPosters[i].statusCount; } var temp = []; for(var x = 0; x < users.length; ++x) { // Create the current user to contain the user data, and how many posts they've made today var curUser = {}; curUser.info = users[x]; curUser.statusCount = statusCounts[users[x].username]; temp.push(curUser); } return temp; }] }, controller: 'TopPostersController' }); }]); ================================================ FILE: web/src/main/webapp/app/components/account/AccountView.html ================================================ ================================================ FILE: web/src/main/webapp/app/components/account/FormController.js ================================================ AccountModule.controller('FormController', ['$scope', function($scope) { // Forwards from the parent state to the default child state. $scope.$on('$stateChangeSuccess', function(event, toState) { if(toState.name === 'tatami.account.groups') { $scope.$state.go('tatami.account.groups.main.top.list'); } else if(toState.name === 'tatami.account.tags') { $scope.$state.go('tatami.account.tags.following'); } else if(toState.name === 'tatami.account.users') { $scope.$state.go('tatami.account.users.following'); } }); }]); ================================================ FILE: web/src/main/webapp/app/components/account/FormView.html ================================================
    ================================================ FILE: web/src/main/webapp/app/components/account/files/FilesController.js ================================================ FilesModule.controller('FilesController', [ '$scope', '$translate', 'FilesService', 'attachmentQuota', 'fileList', 'ngToast', function($scope, $translate, FilesService, attachmentQuota, fileList, ngToast) { // Initialize stuff $scope.quota = attachmentQuota[0]; $scope.fileList = fileList; /** * Allows us to delete the supplied attachment * @param attachment */ $scope.delete = function(attachment, removalIndex) { FilesService.delete({attachmentId: attachment}, { }, function() { $scope.fileList.splice(removalIndex, 1); $scope.$state.reload(); ngToast.create($translate.instant('tatami.form.deleted')); }); }; $scope.getImgPath = function(thumbnail, attachmentId, filename) { if(thumbnail) { return '/tatami/thumbnail/' + attachmentId + '/' + filename; } else { return '/img/document_icon.png'; } }; $scope.setColor = function() { if($scope.quota <= 100 && $scope.quota >= 80) { return "progress-bar progress-bar-danger"; } else if($scope.quota < 80 && $scope.quota > 50) { return "progress-bar progress-bar-warning"; } else { return "progress-bar progress-bar-success"; } }; }]); ================================================ FILE: web/src/main/webapp/app/components/account/files/FilesModule.js ================================================ var FilesModule = angular.module('FilesModule', []); ================================================ FILE: web/src/main/webapp/app/components/account/files/FilesService.js ================================================ FilesModule.factory('FilesService', function($resource) { return $resource( '/tatami/rest/attachments/:attachmentId', { }, { 'getQuota': { method: 'GET', isArray: true, url: '/tatami/rest/attachments/quota' } }); }); ================================================ FILE: web/src/main/webapp/app/components/account/files/FilesView.html ================================================

    {{ quota }}%
    {{ file.filename }} {{ file.size / 1000 }} kb {{ file.prettyPrintCreationDate }}
    ================================================ FILE: web/src/main/webapp/app/components/account/groups/GroupsController.js ================================================ GroupsModule.controller('GroupsController', [ '$scope', 'GroupService', 'SearchService', 'userGroups', 'profileInfo', function($scope, GroupService, SearchService, userGroups, profileInfo) { $scope.userGroups = userGroups; /** * Determines the current look of the group page * When createGroup is true, we display the group creation view */ $scope.current = { searchString: $scope.$stateParams.q }; $scope.search = function() { // Update the route $scope.$state.transitionTo('tatami.account.groups.main.top.search', { q: $scope.current.searchString }, { location: true, inherit: true, relative: $scope.$state.$current, notify: false }); // Update the group data SearchService.query({term: 'groups', q: $scope.current.searchString }, function(result) { // Now update the user groups $scope.userGroups = result; }); }; $scope.joinLeaveGroup = function(group) { if(!group.member) { GroupService.join( { groupId: group.groupId, username: profileInfo.username }, null, function(response) { if(response.isMember) { $scope.$state.reload(); } } ); } else { GroupService.leave( { groupId: group.groupId, username: profileInfo.username }, null, function(response) { if(response) { $scope.$state.reload(); } } ); } }; } ]); ================================================ FILE: web/src/main/webapp/app/components/account/groups/GroupsModule.js ================================================ var GroupsModule = angular.module('GroupsModule', []); ================================================ FILE: web/src/main/webapp/app/components/account/groups/GroupsView.html ================================================ ================================================ FILE: web/src/main/webapp/app/components/account/groups/creation/GroupsCreateController.js ================================================ GroupsModule.controller('GroupsCreateController', ['$scope', '$translate', 'GroupService', 'ngToast', function($scope, $translate, GroupService, ngToast) { $scope.current = {}; /** * When creating a group, the POST requires this payload * @type {{name: string, description: string, publicGroup: boolean, archivedGroup: boolean}} */ $scope.groups = { name: "", description: "", publicGroup: true, archivedGroup: false }; /** * Allows the user to toggle the group creation view */ $scope.newGroup = function() { $scope.current.createGroup = !$scope.current.createGroup; }; /** * Allows the user to cancel group creation */ $scope.cancelGroupCreate = function() { $scope.reset(); }; /** * Resets the group creation view */ $scope.reset = function() { $scope.groups = {}; $scope.current.createGroup = false; }; /** * Creates a new group on the server */ $scope.createNewGroup = function() { GroupService.save($scope.groups, function() { $scope.reset(); $scope.$state.reload(); // Alert user of new group creation ngToast.create({ content: $translate.instant('tatami.account.groups.save') }); }, function() { ngToast.create({ content: $translate.instant('tatami.form.fail'), class: 'danger' }); }); }; }]); ================================================ FILE: web/src/main/webapp/app/components/account/groups/creation/GroupsCreateView.html ================================================


    ================================================ FILE: web/src/main/webapp/app/components/account/groups/list/GroupListController.js ================================================ GroupsModule.controller('GroupController', ['$scope', function($scope) { if($scope.$state.name === 'tatami.account.groups.main') { $scope.$state.go('tatami.account.groups.main.top.list'); } }]); ================================================ FILE: web/src/main/webapp/app/components/account/groups/list/GroupsListView.html ================================================
    {{ group.name }} {{ group.counter }}
    ================================================ FILE: web/src/main/webapp/app/components/account/groups/manage/GroupsManageController.js ================================================ GroupsModule.controller('GroupsManageController', ['$scope', 'group', 'GroupService', 'UserService', 'members', function($scope, group, GroupService, UserService, members) { $scope.group = group; $scope.members = members; $scope.searchedMembers = {}; $scope.current = { searchString: '' }; $scope.updateGroup = function() { GroupService.update( { groupId: $scope.group.groupId }, $scope.group); }; $scope.removeUser = function(member) { GroupService.leave( { groupId: $scope.group.groupId, username: member.username }, null, function() { $scope.$state.reload(); } ); }; $scope.search = function() { if($scope.current.searchString) { UserService.searchUsers({ term: 'search', q: $scope.current.searchString }, function(result) { $scope.searchedMembers = result; }); } else { $scope.searchedMembers = {}; } }; $scope.addUser = function(member) { GroupService.join( { groupId: $scope.group.groupId, username: member.username }, null, function() { $scope.$state.reload(); } ); }; }]); ================================================ FILE: web/src/main/webapp/app/components/account/groups/manage/GroupsManageView.html ================================================



    {{ member.firstName + ' ' + member.lastName }}
    @{{ member.username }}

    {{ 'tatami.account.groups.' + member.role | lowercase | translate }}

    ================================================ FILE: web/src/main/webapp/app/components/account/password/PasswordController.js ================================================ PasswordModule.controller('PasswordController', ['$scope', '$translate', 'PasswordService', 'ngToast', function($scope, $translate, PasswordService, ngToast) { $scope.password = { oldPassword: '', newPassword: '', newPasswordConfirmation: '' }; /** * These booleans will determine whether popovers should be displayed */ $scope.status = { oldEmpty: false, newEmpty: false, confirmWrong: false, confirmChange: false }; $scope.changePassword = function() { if($scope.password.oldPassword === '') { // Display a popover on Old password field $scope.status.oldEmpty = true; } else if($scope.password.newPassword === '') { // Display a popover on the new password field $scope.status.newEmpty = true; } else if($scope.password.newPassword !== $scope.password.newPasswordConfirmation) { // Display a popover on password confirmation $scope.status.confirmWrong = true; } else { // Everything is alright, we can send the new password PasswordService.save($scope.password, function() { // Clear the fields after we have changed the password $scope.reset(); // Alert user that the password has been changed ngToast.create($translate.instant('tatami.account.password.save')); }, function() { ngToast.create({ content: $translate.instant('tatami.form.fail'), class: 'danger' }); }); } }; $scope.reset = function() { $scope.password = {}; $scope.status.oldEmpty = false; $scope.status.newEmpty = false; $scope.status.confirmWrong = false; }; }]); ================================================ FILE: web/src/main/webapp/app/components/account/password/PasswordModule.js ================================================ var PasswordModule = angular.module('PasswordModule', []); ================================================ FILE: web/src/main/webapp/app/components/account/password/PasswordService.js ================================================ PasswordModule.factory('PasswordService', function($resource) { return $resource('/tatami/rest/account/password'); }); ================================================ FILE: web/src/main/webapp/app/components/account/password/PasswordView.html ================================================

    ================================================ FILE: web/src/main/webapp/app/components/account/preferences/PreferencesController.js ================================================ PreferencesModule.controller('PreferencesController', [ '$scope', '$translate', 'PreferencesService', 'prefs', 'ngToast', function($scope, $translate, PreferencesService, prefs, ngToast) { $scope.prefs = prefs; // Update user preferences $scope.savePrefs = function() { $scope.validatePrefs(); PreferencesService.save($scope.prefs, function() { ngToast.create($translate.instant('tatami.account.preferences.save')); }, function() { ngToast.create({ content: $translate.instant('tatami.form.fail'), class: 'danger' }); }); }; $scope.validatePrefs = function() { for(var pref in $scope.prefs) { if($scope.prefs.hasOwnProperty(pref) && $scope.prefs[pref] === null) { if('rssUid' === $scope.prefs[pref]) { continue; } $scope.prefs[pref] = false; } } }; }]); ================================================ FILE: web/src/main/webapp/app/components/account/preferences/PreferencesModule.js ================================================ var PreferencesModule = angular.module('PreferencesModule', []); ================================================ FILE: web/src/main/webapp/app/components/account/preferences/PreferencesService.js ================================================ PreferencesModule.factory('PreferencesService', function($resource) { return $resource('/tatami/rest/account/preferences'); }); ================================================ FILE: web/src/main/webapp/app/components/account/preferences/PreferencesView.html ================================================

    {{ 'tatami.account.preferences.notification.rss.link' | translate }}
    ================================================ FILE: web/src/main/webapp/app/components/account/profile/ProfileController.js ================================================ ProfileModule.controller('ProfileController', ['$scope', '$upload', '$translate', 'ProfileService', 'profileInfo', 'userLogin', 'ngToast', function($scope, $upload, $translate, ProfileService, profileInfo, userLogin, ngToast) { // Current state of the view $scope.current = { avatar: [] }; // Status of the current upload $scope.uploadStatus = { isUploading: false, progress: 0 }; // Resolve the user data, profileInfo is inherited from account state // Since profileInfo is a resolve from the parent state, updating the model will cause // the first and last name in the side bar and text area to sync. Is this undesired? $scope.userProfile = profileInfo; $scope.userLogin = userLogin.login; // Update the user information $scope.updateUser = function() { ProfileService.update($scope.userProfile, function() { ngToast.create({ content: $translate.instant('tatami.account.profile.save') }); }, function() { ngToast.create({ content: $translate.instant('tatami.form.fail'), class: 'danger' }); }); }; // Handle user avatar changes based on drag and drop $scope.$watch('current.avatar', function() { for(var i = 0; i < $scope.current.avatar.length; ++i){ var file = $scope.current.avatar[i]; $scope.uploadStatus.isUploading = true; $scope.upload = $upload.upload({ url: '/tatami/rest/fileupload/avatar', file: file, fileFormDataName: 'uploadFile' }).progress(function(evt) { $scope.uploadStatus.progress = parseInt(100.0 * evt.loaded / evt.total); }).success(function() { $scope.uploadStatus.isUploading = false; $scope.uploadStatus.progress = 0; $scope.$state.reload(); }).error(function() { $scope.uploadStatus.isUploading = false; $scope.uploadStatus.progress = 0; }); } }); } ]); ================================================ FILE: web/src/main/webapp/app/components/account/profile/ProfileModule.js ================================================ var ProfileModule = angular.module('ProfileModule', []); ================================================ FILE: web/src/main/webapp/app/components/account/profile/ProfileView.html ================================================

    ================================================ FILE: web/src/main/webapp/app/components/account/tags/TagsController.js ================================================ TagsModule.controller('TagsController', [ '$scope', 'TagService', 'SearchService', 'tagList', function($scope, TagService, SearchService, tagList) { $scope.current = { searchString: '' }; $scope.tags = tagList; /** * Follows an unfollowed tag, or unfollows a followed tag, depending on the current state * @param tag */ $scope.followTag = function(tag, index) { TagService.follow( { tag: tag.name }, { name: tag.name, followed: !tag.followed, trendingUp: tag.trendingUp }, function(response) { $scope.tags[index].followed = response.followed; }); }; $scope.search = function() { // Update the route $scope.$state.transitionTo('tatami.account.tags.search', { q: $scope.current.searchString }, { location: true, inherit: true, relative: $scope.$state.$current, notify: false }); // Update the tag data data if($scope.current.searchString.length === 0) { $scope.tags = {}; } else{ SearchService.query({term: 'tags', q: $scope.current.searchString }, function(result) { // Now update the tags $scope.tags = result; }); } }; } ]); ================================================ FILE: web/src/main/webapp/app/components/account/tags/TagsModule.js ================================================ var TagsModule = angular.module('TagsModule', []); ================================================ FILE: web/src/main/webapp/app/components/account/tags/TagsView.html ================================================ ================================================ FILE: web/src/main/webapp/app/components/account/topPosters/TopPostersController.js ================================================ TopPostersModule.controller('TopPostersController', ['$scope', 'topPosters', 'UserService', 'userData', function($scope, topPosters, UserService, userData) { $scope.topPosters = userData; $scope.topPosters.sort(function(a, b) { return a.statusCount < b.statusCount ? 1 : -1; }); }]); ================================================ FILE: web/src/main/webapp/app/components/account/topPosters/TopPostersModule.js ================================================ var TopPostersModule = angular.module('TopPostersModule', []); ================================================ FILE: web/src/main/webapp/app/components/account/topPosters/TopPostersView.html ================================================ ================================================ FILE: web/src/main/webapp/app/components/account/users/UsersController.js ================================================ UsersModule.controller('UsersController', ['$scope', 'usersList', 'SearchService', 'UserService', 'userRoles', function($scope, usersList, SearchService, UserService, userRoles) { $scope.isAdmin = userRoles.roles.indexOf('ROLE_ADMIN') !== -1; /** * Information about the current state of the view * @type {{searchString: string}} */ $scope.current = { searchString: $scope.$stateParams.q }; // usersList is resolved during routing $scope.usersList = usersList; $scope.deactivate = function(user, index) { UserService.deactivate({ username: user.username }, { activate: true }, function(response) { $scope.usersList[index].activated = response.activated; }); }; /** * Change the state (so the url contains the search parameter), and updates * the user data based on the search term. */ $scope.search = function() { // Update the route $scope.$state.transitionTo('tatami.account.users.search', { q: $scope.current.searchString }, { location: true, inherit: true, relative: $scope.$state.$current, notify: false }); // Now update the users based on the search term SearchService.query({ term: 'users', q: $scope.current.searchString }, function(result) { $scope.usersList = result; }); }; $scope.followUser = function(user) { UserService.follow({ username: user.username }, { friend: !user.friend, friendShip: true }, function() { $scope.$state.reload(); } ); }; }]); ================================================ FILE: web/src/main/webapp/app/components/account/users/UsersModule.js ================================================ var UsersModule = angular.module('UsersModule', []); ================================================ FILE: web/src/main/webapp/app/components/account/users/UsersView.html ================================================

    {{ 'tatami.account.users.deactivated' | translate }} {{ user.firstName + ' ' + user.lastName }}
    @{{ user.username }}

    ================================================ FILE: web/src/main/webapp/app/components/account/users/directives/UserAccountController.js ================================================ UsersModule.factory('UserAccountController', ['$scope', function() { }]); ================================================ FILE: web/src/main/webapp/app/components/account/users/directives/UserAccountDirective.js ================================================ UsersModule.directive('tatamiAccountUser', function() { return { restrict: 'E', controller: 'UsersController', scope: { user: '=', isAdmin: '=' }, templateUrl: '/app/components/account/users/directives/UserAccountView.html' }; }); ================================================ FILE: web/src/main/webapp/app/components/account/users/directives/UserAccountView.html ================================================

    {{ 'tatami.account.users.deactivated' | translate }} {{ user.firstName + ' ' + user.lastName }}
    @{{ user.username }}

    ================================================ FILE: web/src/main/webapp/app/components/admin/AdminController.js ================================================ AdminModule.controller('AdminController', [ '$scope', '$translate', 'AdminService', 'adminData', function($scope, $translate, AdminService, adminData) { $scope.adminData = adminData; $scope.reindex = function() { if(confirm($translate.instant('tatami.admin.confirm'))) { AdminService.save({ options: 'reindex' }); $scope.$state.go('admin', { message: 'reindex' }); } }; }]); ================================================ FILE: web/src/main/webapp/app/components/admin/AdminModule.js ================================================ var AdminModule = angular.module('AdminModule', []); AdminModule.config(['$stateProvider', '$urlRouterProvider', function($stateProvider) { $stateProvider .state('admin',{ url: '/admin?message', templateUrl: '/app/components/admin/AdminView.min.html', resolve: { adminData: ['AdminService', '$state', function(AdminService, $state) { return AdminService.get().$promise.then(function(success) { return success; }, function(err) { if(err.status === 500) { $state.transitionTo('tatami.accessdenied', null, { location: false }); } }); }] }, controller: 'AdminController' }); }]); ================================================ FILE: web/src/main/webapp/app/components/admin/AdminService.js ================================================ AdminModule.factory('AdminService', ['$resource', function($resource) { return $resource('/tatami/admin/:options', { options: '@options' }); }]); ================================================ FILE: web/src/main/webapp/app/components/admin/AdminView.html ================================================

    {{ domain.name }} {{ domain.numberOfUsers }}

    {{ key }} {{ value }}

    ================================================ FILE: web/src/main/webapp/app/components/home/HomeModule.js ================================================ var HomeModule = angular.module('HomeModule', [ 'HomeSidebarModule', 'ProfileSidebarModule', 'ngSanitize', 'angularMoment', 'infinite-scroll' ]); HomeModule.run(['UserSession', '$rootScope', '$location', '$interval', '$state', '$document', function (UserSession, $rootScope, $location, $interval, $state) { $interval(function () { //if the user is logged in, not at the login page, and inactive... if (UserSession.isAuthenticated() && UserSession.isUserResolved()) { try { UserSession.authenticate(true); } catch (err) { UserSession.clearSession(); $state.go('tatami.login.main'); alert('User logged out due to session expiration.'); } } }, 1000 * 60 * 10); //10 minutes }]); HomeModule.config(['$stateProvider', function ($stateProvider) { $stateProvider .state('tatami.home', { url: '/home', abstract: true, templateUrl: 'app/components/home/HomeView.min.html', resolve: { profile: ['ProfileService', function (ProfileService) { return ProfileService.get().$promise; }], userRoles: ['$http', function ($http) { return $http({method: 'GET', url: '/tatami/rest/account/admin'}).then(function (result) { return result.data; }); }] } }) .state('tatami.home.status', { url: '/status/:statusId', views: { 'homeBodyContent@tatami.home': { templateUrl: 'app/components/home/status/StatusView.min.html', controller: 'StatusController' } }, resolve: { status: ['StatusService', '$stateParams', '$q', function (StatusService, $stateParams, $q) { return StatusService.get({statusId: $stateParams.statusId}) .$promise.then( function (response) { if (angular.equals({}, response.toJSON())) { return $q.reject(); } return response; }); }], context: ['StatusService', '$stateParams', function (StatusService, $stateParams) { return StatusService.getDetails({statusId: $stateParams.statusId}).$promise; }] } }) .state('tatami.home.search', { url: '/search/:searchTerm', views: { 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/search/SearchHeaderView.min.html', controller: 'SearchHeaderController' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/status/withoutContext/StatusListView.min.html', controller: 'StatusListController' } }, resolve: { statuses: ['SearchService', '$stateParams', function (SearchService, $stateParams) { return SearchService.query({term: 'status', q: $stateParams.searchTerm}).$promise; }], showModal: function () { return false; } } }) //state for all views that use home sidebar .state('tatami.home.home', { url: '^/home', abstract: true, resolve: { groups: ['GroupService', function (GroupService) { return GroupService.query().$promise; }], tags: ['TagService', function (TagService) { return TagService.query({popular: true}).$promise; }], suggestions: ['UserService', function (UserService) { return UserService.getSuggestions().$promise; }], showModal: function () { return false; } } }) .state('tatami.home.home.timeline', { url: '/timeline', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/home/HomeSidebarView.min.html', controller: 'HomeSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/timeline/TimelineHeaderView.min.html' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/status/withoutContext/StatusListView.min.html', controller: 'StatusListController' } }, resolve: { statuses: ['StatusService', function (StatusService) { return StatusService.getHomeTimeline().$promise; }], showModal: ['statuses', function (statuses) { return statuses.length === 0; }] } }) .state('tatami.home.home.timeline.presentation', { url: '', onEnter: ['$stateParams', '$modal', function ($stateParams, $modal) { $modal.open({ templateUrl: 'app/components/home/welcome/WelcomeView.min.html', controller: 'WelcomeController' }); }] }) .state('tatami.home.home.mentions', { url: '/mentions', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/home/HomeSidebarView.min.html', controller: 'HomeSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/timeline/TimelineHeaderView.min.html' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/status/withoutContext/StatusListView.min.html', controller: 'StatusListController' } }, resolve: { statuses: ['HomeService', function (HomeService) { return HomeService.getMentions().$promise; }] } }) .state('tatami.home.home.favorites', { url: '/favorites', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/home/HomeSidebarView.min.html', controller: 'HomeSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/timeline/TimelineHeaderView.min.html' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/status/withoutContext/StatusListView.min.html', controller: 'StatusListController' } }, resolve: { statuses: ['HomeService', function (HomeService) { return HomeService.getFavorites().$promise; }] } }) .state('tatami.home.home.company', { url: '/company', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/home/HomeSidebarView.min.html', controller: 'HomeSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/timeline/TimelineHeaderView.min.html' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/status/withoutContext/StatusListView.min.html', controller: 'StatusListController' } }, resolve: { statuses: ['HomeService', function (HomeService) { return HomeService.getCompanyTimeline().$promise; }] } }) .state('tatami.home.home.tag', { url: '/tag/:tag', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/home/HomeSidebarView.min.html', controller: 'HomeSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/tag/TagHeaderView.min.html', controller: 'TagHeaderController' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/status/withoutContext/StatusListView.min.html', controller: 'StatusListController' } }, resolve: { tag: ['TagService', '$stateParams', function (TagService, $stateParams) { return TagService.get({tag: $stateParams.tag}).$promise; }], statuses: ['TagService', '$stateParams', function (TagService, $stateParams) { return TagService.getTagTimeline({tag: $stateParams.tag}).$promise; }] } }) .state('tatami.home.home.group', { url: '/group/:groupId', abstract: true, resolve: { group: ['GroupService', '$stateParams', function (GroupService, $stateParams) { return GroupService.get({groupId: $stateParams.groupId}).$promise; }] } }) .state('tatami.home.home.group.statuses', { url: '/statuses', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/home/HomeSidebarView.min.html', controller: 'HomeSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/group/GroupHeaderView.min.html', controller: 'GroupHeaderController' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/status/withoutContext/StatusListView.min.html', controller: 'StatusListController' } }, resolve: { statuses: ['GroupService', '$stateParams', function (GroupService, $stateParams) { return GroupService.getStatuses({groupId: $stateParams.groupId}).$promise; }] } }) .state('tatami.home.home.group.members', { url: '/members', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/home/HomeSidebarView.min.html', controller: 'HomeSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/group/GroupHeaderView.min.html', controller: 'GroupHeaderController' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/user/UserListView.min.html', controller: 'UserListController' } }, resolve: { users: ['GroupService', '$stateParams', function (GroupService, $stateParams) { return GroupService.getMembers({groupId: $stateParams.groupId}).$promise; }] } }) //state for all views that use profile sidebar .state('tatami.home.profile', { url: '/profile/:username', abstract: true, resolve: { user: ['UserService', '$stateParams', function (UserService, $stateParams) { return UserService.get({username: $stateParams.username}).$promise; }], tags: ['TagService', '$stateParams', function (TagService, $stateParams) { return TagService.query({popular: true, user: $stateParams.username}).$promise; }] } }) .state('tatami.home.profile.statuses', { url: '/statuses', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/profile/ProfileSidebarView.min.html', controller: 'ProfileSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/profile/ProfileHeaderView.min.html', controller: 'ProfileHeaderController' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/status/withoutContext/StatusListView.min.html', controller: 'StatusListController' } }, resolve: { statuses: ['StatusService', '$stateParams', function (StatusService, $stateParams) { return StatusService.getUserTimeline({username: $stateParams.username}).$promise; }], showModal: function () { return false; } } }) .state('tatami.home.profile.following', { url: '/following', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/profile/ProfileSidebarView.min.html', controller: 'ProfileSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/profile/ProfileHeaderView.min.html', controller: 'ProfileHeaderController' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/user/UserListView.min.html', controller: 'UserListController' } }, resolve: { users: ['UserService', '$stateParams', function (UserService, $stateParams) { return UserService.getFollowing({username: $stateParams.username}).$promise; }] } }) .state('tatami.home.profile.followers', { url: '/followers', views: { 'homeSide@tatami.home': { templateUrl: 'app/shared/sidebars/profile/ProfileSidebarView.min.html', controller: 'ProfileSidebarController' }, 'homeBodyHeader@tatami.home': { templateUrl: 'app/components/home/profile/ProfileHeaderView.min.html', controller: 'ProfileHeaderController' }, 'homeBodyContent@tatami.home': { templateUrl: 'app/shared/lists/user/UserListView.min.html', controller: 'UserListController' } }, resolve: { users: ['UserService', '$stateParams', function (UserService, $stateParams) { return UserService.getFollowers({username: $stateParams.username}).$promise; }] } }); } ]); ================================================ FILE: web/src/main/webapp/app/components/home/HomeView.html ================================================
    ================================================ FILE: web/src/main/webapp/app/components/home/group/GroupHeaderController.js ================================================ HomeModule.controller('GroupHeaderController', ['$scope', 'GroupService', 'profile', 'group', function($scope, GroupService, profile, group) { $scope.profile = profile; $scope.group = group; $scope.joinLeaveGroup = function() { if(!$scope.group.member) { GroupService.join( { groupId: $scope.group.groupId, username: $scope.profile.username }, null, function(response) { if(response.isMember) { $scope.group.member = response.isMember; $scope.$state.reload(); } } ); } else { GroupService.leave( { groupId: $scope.group.groupId, username: $scope.profile.username }, null, function(response) { if(response) { $scope.group.member = !response; $scope.$state.reload(); } } ); } }; } ]); ================================================ FILE: web/src/main/webapp/app/components/home/group/GroupHeaderView.html ================================================

    {{ group.name }}: {{ group.description }}

    ================================================ FILE: web/src/main/webapp/app/components/home/profile/ProfileHeaderController.js ================================================ HomeModule.controller('ProfileHeaderController', ['$scope', 'UserService', 'user', function($scope, UserService, user) { $scope.user = user; $scope.followUnfollowUser = function() { UserService.follow( { username: $scope.user.username }, { friend: !$scope.user.friend, friendShip: true }, function(response) { $scope.user.friend = response.friend; $scope.$state.reload(); }); }; } ]); ================================================ FILE: web/src/main/webapp/app/components/home/profile/ProfileHeaderView.html ================================================

    {{ 'tatami.home.profile.followsYou' | translate | uppercase }} {{ 'tatami.home.profile.deactivatedUser' | translate | uppercase }} {{ 'tatami.home.profile.followsYou' | translate | uppercase }} {{ 'tatami.home.profile.deactivatedUser' | translate | uppercase }} {{ 'tatami.home.profile.followsYou' | translate | uppercase }} {{ 'tatami.home.profile.deactivatedUser' | translate | uppercase }}

    ================================================ FILE: web/src/main/webapp/app/components/home/search/SearchHeaderController.js ================================================ HomeModule.controller('SearchHeaderController', ['$scope', '$stateParams', function($scope, $stateParams) { $scope.searchTerm = $stateParams.searchTerm; } ]); ================================================ FILE: web/src/main/webapp/app/components/home/search/SearchHeaderView.html ================================================

    "{{ searchTerm }}"

    ================================================ FILE: web/src/main/webapp/app/components/home/status/StatusController.js ================================================ HomeModule.controller('StatusController', [ '$scope', '$translate', 'StatusService', 'profile', 'status', 'context', 'userRoles', function($scope, $translate, StatusService, profile, status, context, userRoles) { $scope.isAdmin = userRoles.roles.indexOf('ROLE_ADMIN') !== -1; $scope.profile = profile; if(context.discussionStatuses.length === 0) { $scope.statuses = [status]; } else { $scope.statuses = context.discussionStatuses; } try { for(var i = 0; i <= context.discussionStatuses.length; i++) { if (status.statusDate < $scope.statuses[i].statusDate) { $scope.statuses.splice(i, 0, status); break; } } } catch(err) { $scope.statuses.push(status); } $scope.isOneDayOrMore = function(date) { return moment().diff(moment(date), 'days', true) >= 1; }; $scope.getLanguageKey = function() { return $translate.use(); }; $scope.openReplyModal = function(status) { $scope.$state.go($scope.$state.current.name + '.post', { statusIdReply: status.statusId }); }; $scope.favoriteStatus = function(status, index) { StatusService.update({ statusId: status.statusId }, { favorite: !status.favorite }, function(response) { $scope.statuses[index].favorite = response.favorite; }); }; $scope.shareStatus = function(status, index) { StatusService.update({ statusId: status.statusId }, { shared: !status.shareByMe }, function(response) { $scope.statuses[index].shareByMe = response.shareByMe; }); }; $scope.announceStatus = function(status) { StatusService.update({ statusId: status.statusId }, { announced: true }, function() { $scope.$state.reload(); }); }; $scope.deleteStatus = function(status, index) { // Need a confirmation modal here StatusService.delete({ statusId: status.statusId }, null, function() { if($scope.$stateParams.statusId === status.statusId) { $scope.$state.transitionTo('tatami.home.home.timeline'); } else { $scope.statuses.splice(index, 1); } }); }; $scope.getShares = function(status, index) { if(status.type === 'STATUS' && status.shares === null) { StatusService.getDetails({ statusId: status.statusId }, null, function(response) { $scope.statuses[index].shares = response.sharedByLogins; }); } }; } ]); ================================================ FILE: web/src/main/webapp/app/components/home/status/StatusView.html ================================================
    ================================================ FILE: web/src/main/webapp/app/components/home/tag/TagHeaderController.js ================================================ HomeModule.controller('TagHeaderController', ['$scope', 'TagService', 'tag', function($scope, TagService, tag) { $scope.tag = tag; $scope.followUnfollowTag = function() { TagService.follow( { tag: $scope.tag.name }, { name: $scope.tag.name, followed: !$scope.tag.followed, trendingUp: $scope.tag.trendingUp }, function(response) { $scope.tag.followed = response.followed; $scope.$state.reload(); } ); }; } ]); ================================================ FILE: web/src/main/webapp/app/components/home/tag/TagHeaderView.html ================================================

    #{{ tag.name }}

    ================================================ FILE: web/src/main/webapp/app/components/home/timeline/TimelineHeaderView.html ================================================ ================================================ FILE: web/src/main/webapp/app/components/home/welcome/WelcomeController.js ================================================ HomeModule.controller('WelcomeController', ['$scope', '$modalInstance', '$rootScope', function($scope, $modalInstance, $rootScope) { $scope.close = function() { $modalInstance.dismiss(); }; $scope.launchPresentation = function() { $rootScope.$broadcast('start-tour'); $modalInstance.dismiss(); }; // Handles closing the modal via escape and clicking outside the modal $modalInstance.result.finally(function() { $scope.$state.go('tatami.home.home.timeline'); }); }]); ================================================ FILE: web/src/main/webapp/app/components/home/welcome/WelcomeView.html ================================================
    ================================================ FILE: web/src/main/webapp/app/components/login/LoginController.js ================================================ LoginModule.controller('LoginController', ['$scope', function($scope) { console.log('here'); $scope.loginFailed = false; }]); ================================================ FILE: web/src/main/webapp/app/components/login/LoginModule.js ================================================ var LoginModule = angular.module('LoginModule', []); LoginModule.config(['$stateProvider', function($stateProvider) { $stateProvider .state('tatami.login', { url: '', abstract: true, templateUrl: 'app/components/login/LoginView.min.html', }) .state('tatami.login.main', { url: '/login?action', views: { 'manualLogin': { templateUrl: '/app/components/login/manual/ManualLoginView.min.html', controller: 'ManualLoginController' }, 'recoverPassword': { templateUrl: '/app/components/login/recoverPassword/RecoverPasswordView.min.html', controller: 'RecoverPasswordController' }, 'googleLogin': { templateUrl: '/app/components/login/google/GoogleLoginView.min.html', controller: 'GoogleLoginController' }, 'register': { templateUrl: '/app/components/login/register/RegisterView.min.html', controller: 'RegisterController' } }, data: { public: true } }) .state('tatami.registration', { url: '/register?key', templateUrl: '/app/components/login/email/EmailRegistration.min.html', controller: 'EmailRegistrationController', resolve: { update: ['RegistrationService', '$stateParams', function(RegistrationService, $stateParams) { return RegistrationService.getUpdate({ register: 'register', key: $stateParams.key }).$promise; }] }, data: { public: true } }); }]); ================================================ FILE: web/src/main/webapp/app/components/login/LoginView.html ================================================

    ================================================ FILE: web/src/main/webapp/app/components/login/RegistrationService.js ================================================ LoginModule.factory('RegistrationService', ['$resource', function($resource) { return $resource('/tatami/:register', null, { 'resetPassword': { method: 'POST', url: '/tatami/lostpassword', headers : {'Content-Type': 'application/x-www-form-urlencoded'} }, 'getUpdate': { method: 'GET' }, 'registerUser': { method: 'POST', url: '/tatami/register', headers: {'Content-Type': 'application/x-www-form-urlencoded'} } }); }]); ================================================ FILE: web/src/main/webapp/app/components/login/email/EmailRegistration.html ================================================



    ================================================ FILE: web/src/main/webapp/app/components/login/email/EmailRegistrationController.js ================================================ LoginModule.controller('EmailRegistrationController', [function() { }]); ================================================ FILE: web/src/main/webapp/app/components/login/google/GoogleLoginController.js ================================================ LoginModule.controller('GoogleLoginController', ['$scope', '$http', 'UserSession', function($scope, $http, UserSession) { if (UserSession.isAuthenticated()){ $scope.$state.go('tatami.home.home.timeline'); } $scope.logout = function() { $http.get('/tatami/logout') .success(function() { UserSession.clearSession(); $scope.$state.go('tatami.login.main'); }); }; }]); ================================================ FILE: web/src/main/webapp/app/components/login/google/GoogleLoginView.html ================================================ ================================================ FILE: web/src/main/webapp/app/components/login/manual/ManualLoginController.js ================================================ LoginModule.controller('ManualLoginController', ['$scope', '$rootScope', '$http', 'AuthenticationService', 'UserSession', function ($scope, $rootScope, $http, AuthenticationService, UserSession) { $scope.user = {}; $scope.login = function () { $http({ method: 'POST', url: '/tatami/authentication', transformRequest: function (obj) { var str = []; for (var p in obj) str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); return str.join("&"); }, data: { j_username: $scope.user.email, j_password: $scope.user.password, _spring_security_remember_me: $scope.user.remember }, headers: {'Content-Type': 'application/x-www-form-urlencoded'} }) .success(function (data) { if (data.action === 'loginFailure') { $scope.$state.go('tatami.login.main', {action: data.action}); } else { // The user has logged in, authenticate them UserSession.setLoginState(true); // Redirect the user to the state they tried to access now that they are logged in if (angular.isDefined($rootScope.returnToState) || angular.isDefined($rootScope.returnToStateParams)) { // redirect to previous state $scope.$state.go($rootScope.returnToState.name, $rootScope.returnToParams); } // If they were not trying to access a specific state, send them to the home state else { $scope.$state.go('tatami.home.home.timeline'); } } }) .error(function (data) { console.log('showing banner'); $scope.loginFailed = true; }); }; }]); ================================================ FILE: web/src/main/webapp/app/components/login/manual/ManualLoginView.html ================================================

    ================================================ FILE: web/src/main/webapp/app/components/login/recoverPassword/RecoverPasswordController.js ================================================ LoginModule.controller('RecoverPasswordController', ['$scope', 'RegistrationService', function($scope, RegistrationService) { $scope.user = {}; $scope.resetPassword = function() { var data = 'email=' + $scope.user.email; RegistrationService.resetPassword(data).$promise.then(function(success) { $scope.$state.go('tatami.login.main', { action: success.action }); $scope.user.email = ''; }, function(err) { console.log(err); }); }; }]); ================================================ FILE: web/src/main/webapp/app/components/login/recoverPassword/RecoverPasswordView.html ================================================

    ================================================ FILE: web/src/main/webapp/app/components/login/register/RegisterController.js ================================================ LoginModule.controller('RegisterController', ['$scope', 'RegistrationService', function($scope, RegistrationService) { $scope.user = {}; $scope.registerUser = function() { var data = 'email=' + $scope.user.email; RegistrationService.registerUser(data).$promise.then(function(success) { $scope.$state.go('tatami.login.main', { action: success.action }); $scope.user.email = ''; }, function(err) { console.log(err); }); }; }]); ================================================ FILE: web/src/main/webapp/app/components/login/register/RegisterView.html ================================================

    ================================================ FILE: web/src/main/webapp/app/shared/configs/MarkedConfig.js ================================================ marked.setOptions({ gfm: true, pedantic: false, sanitize: true, highlight: null, urls: { youtube : function(text, url){ var cap; if((cap = /(youtu\.be\/|youtube\.com\/(watch\?(.*&)?v=|(embed|v)\/))([^\?&"'>]+)/.exec(url))){ return ''; } }, vimeo : function(text, url){ var cap; if((cap = /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/.exec(url))){ return ''; } }, dailymotion : function(text, url){ var cap; if((cap = /^.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/.exec(url))){ return ''; } }, gist : function(text, url){ var cap; if((cap = /^.+gist.github.com\/(([A-z0-9-]+)\/)?([0-9A-z]+)/.exec(url))){ $.ajax({ url: cap[0] + '.json', dataType: 'jsonp', success: function(response){ if(response.stylesheet && $('link[href="' + response.stylesheet + '"]').length === 0){ var l = document.createElement("link"), head = document.getElementsByTagName("head")[0]; l.type = "text/css"; l.rel = "stylesheet"; l.href = response.stylesheet; head.insertBefore(l, head.firstChild); } var $elements = $('.gist' + cap[3]); $elements.html(response.div); } }); return '
    '; } } } }); ================================================ FILE: web/src/main/webapp/app/shared/configs/MomentConfig.js ================================================ TatamiApp.run(['$translate', 'amMoment', function($translate, amMoment) { amMoment.changeLocale($translate.use()); }]); moment.locale('en', { relativeTime : { future: "", past: "%s", s: "1s", m: "1m", mm: "%dm", h: "1h", hh: "%dh", d: "1d", dd: "%dd", M: "1mo", MM: "%dmo", y: "1y", yy: "%dy" } }); moment.locale('fr', { relativeTime : { future: "", past: "%s", s: "1 s", m: "1 min", mm: "%d min", h: "1 h", hh: "%d h", d: "1 j", dd: "%d j", M: "1 m", MM: "%d m", y: "1 a", yy: "%d a" } }); ================================================ FILE: web/src/main/webapp/app/shared/configs/TranslateConfig.js ================================================ TatamiApp.config(['$translateProvider', function($translateProvider) { $translateProvider.translations('en', { 'tatami': { 'pageNotFound': 'Page not found.', 'error': 'An error has occurred.', 'welcome': { 'title': 'Welcome to Tatami', 'message': 'Your timeline is empty! Do you need help to learn how to use Tatami? Please click on the button below to launch a presentation.', 'presentation': 'Launch presentation', 'help': { 'title': 'Help', 'line1': 'Welcome to the online help!', 'line2':' Follow the next steps for a tour of the main Tatami features.' }, 'timeline': { 'title': 'Timeline', 'line1': 'This is your timeline. It displays all messages', 'bulletPoint1': 'mentioning you or sent privately to you', 'bulletPoint2': 'sent by users you follow', 'bulletPoint3': 'sent by yourself', 'bulletPoint4': 'sent to group you are subscribed to', 'afterBP1': 'If it\'s empty, don\'t worry, it will get updated as soon as you start following other users!', 'afterBP2': 'When viewing a message, you can reply to it and mark it as a favorite to find it easily later.', 'afterBP3': 'If a message already has some replies, you can see them all by clicking on the timestamp or clicking \'View Conversation\'.' }, 'post': { 'title': 'Posting Messages', 'line1': 'Here is where you write messages you want to share', 'bulletPoint1': 'All messages are public by default. They will be delivered to all users who follow you.', 'bulletPoint2': 'When writing a message you should use <\i>#hashtags. This simply means adding a # at the beginning of important words that can be used to find your message.', 'bulletPoint3': 'When mentioning or replying to other users you should add a @ at the beginning of their name. They will be notified that you are talking to them.' }, 'groups': { 'title': 'Groups', 'line1': 'This is the list of groups you are a member of.', 'line2': 'You can find and subscribe to public groups in the Account/Groups page (top-right menu).', 'line3': 'There are also private groups. You cannot subscribe to these. The owner of the group must add you as a member.' }, 'trends': { 'title': 'Trends', 'line1': 'This list represents the #hashtags that are currently the most often used on Tatami. Use this to discover what\'s going on and what are the hottest topics on Tatami!' }, 'whoToFollow': { 'title': 'Who To Follow', 'line1': 'This is a list of users who share common interests with you and who you could follow.', 'line2': 'If you are a new user, this list is probably empty. Tatami needs some time to learn who you are in order to suggest relevant users.', 'line3': 'And don\'t forget to use #hashtags in your messages. It makes everything easier!' }, 'next': 'Next', 'previous': 'Prev', 'end': 'End' }, // Login View 'login': { 'title': 'Login', 'mainTitle': 'Welcome to Tatami', 'subtitle': 'An open source enterprise social network', 'moreInfo': 'More Info', 'email': 'Email', 'password': 'Password', 'remember': 'Remember me', 'forgotPassword': 'Forgot your password?', 'resetPassword': 'Reset password', 'tos': 'Terms of Service', 'fail': 'Your authentication has failed! Are you sure you used the correct password?', 'passwordEmailSent': 'An email has been sent to you, with instructions to generate a new password.', 'unregisteredEmail': 'This email address is not registered in Tatami.', 'registrationFailure': 'User registration failed. That email already is in use.', 'register': { 'title': 'Register', 'line1': 'A confirmation email will be sent to the address you provide.', 'line2': "Your email's domain will determine the company space you join. For example, users with an email@ippon.fr address will join Ippon's private space.", 'line3': "If you are the first employee of your company to join Tatami, your company's private space will be automatically created." }, 'googleApps': { 'title': 'Google Apps Login', 'line1': 'This feature is for Google Apps for Work users, who have their work domain name managed by Google Apps.', 'link': 'For more information on Google Apps for Work click here.', 'line2': 'Whether or not you already have a Tatami account, you can sign in with your Google Apps account.', 'line3': "Your email will be provided by Google and your email's domain name will be used to allow you to join your company's private space.", 'login': 'Login using Google Apps' }, 'validation': 'E-mail validation', 'passwordSuccess': 'Your email has been validated. Your password will be emailed to you.', 'returnHome': 'Go to the home page', 'registrationEmail': 'Thank you! A registration email has been sent to you.' }, 'about': { // Presentation Page 'presentation': { 'title': 'What is Tatami?', 'devices': 'Devices', 'openSource': 'Open Source', 'row1': { 'title': 'A private, enterprise social network', 'line1': 'Update your status to inform your co-workers', 'line2': "Subscribe to other employees' time lines", 'line3': 'Share important information to your followers', 'line4': 'Discuss and reply to your colleagues', 'line5': 'Put important information into favorites', 'line6': 'Search useful information with our integrated search engine', 'line7': 'Use hashtags to find related information', 'line8': "Go to your co-workers' profiles to see what they are working on", 'line9': 'English and French versions available, adding other languages is easy' }, 'row2': { 'title': 'Works on all devices!', 'line1': 'Dynamic web application (HTML5): nothing to install, except a modern browser!', 'line2': "Works on mobile devices, tablets, or standard computers: the application adapts itself automatically to your device's screen", 'line3': 'Stay connected with your enterprise wherever you are' }, 'row3': { 'title': "Easy installation and integration with your company's IT infrastructure", 'line1': 'Standard Java application', 'line2': 'Your data belongs to you, not to your SaaS vendor!', 'line3': 'Integrates with your LDAP directory', 'line4': 'Integrates with Google Apps', 'line5': 'Fully Open Source, with a business-friendly Apache 2.0 license', 'line6': 'Easy to extend or modify according to your needs', 'line7': 'High performance (based on Apache Cassandra), even on small hardware', 'line8': 'Join the project and submit patches on our Github page:' }, 'row4': { 'title': "Also available in SaaS mode, fully managed by Ippon Technologies", 'line1': "If you do not want to install Tatami in your company, it's easy to use directly", 'line2': 'Secured multi-enterprise mode: every company has its own private space', 'line3': '256 bits SSL encryption: all data transfers are fully secured' }, 'row5': { 'title': 'Need more information on our product?', 'line1': 'Our sales team is looking forward to hearing from you! Call us at +33 01 46 12 48 48 or email us at' } }, // Terms of Service Page 'tos': { 'title': 'Terms of Service' }, // License Page 'license': { 'title': 'Source Code License', 'copyright': 'Copyright' } }, // Home View 'home': { 'timeline': 'Timeline', 'mentions': 'Mentions', 'favorites': 'Favorites', 'companyTimeline': 'Company Timeline', 'newMessage': 'New Message', 'newMessages': 'New Messages', //Top Menu 'menu': { 'logo': 'Ippon Technologies Logo', 'title': 'Tatami', 'about': { 'title': 'About', 'version' : 'Version', 'presentation': 'Presentation', 'tos': 'Terms of Service', 'language': { 'language': 'Language', 'en': 'English', 'fr': 'Français' }, 'license': 'Source Code License', 'github': { 'issues': 'Submit a Bug Report', 'fork': 'Fork Tatami on Github' }, 'ippon': { 'website': 'Ippon Technologies Website', 'blog': 'Ippon Technologies Blog', 'twitter': 'Follow @ippontech on Twitter' } }, 'search': 'Search', 'help': 'Help', 'account': { 'title': 'Account', 'companyTimeline': 'Company Timeline', 'logout': 'Logout' } }, // Post Modal 'post': { 'mandatory': 'Comment is mandatory', 'content': { 'mandatory': 'Please fill out this field.' }, 'update': 'Update your status', 'replyTo': 'Reply', 'preview': 'Preview', 'edit': 'Edit', 'characters': { 'left': 'Characters Left:' }, 'options': 'Options', 'shareLocation': 'Share Location', 'group': 'Group', 'reply': 'Reply to this status', 'files': 'Files', 'drop': { 'file': 'Drop your files here' }, 'markdown': 'Markdown Supported', 'button': 'Post' }, // Home Sidebar View 'sidebar': { 'myGroups': 'My Groups', 'whoToFollow': 'Who To Follow', 'trends': 'Trends', 'public': 'PUB', 'private': 'PVT', 'archived': 'ARC', 'administrator': 'A', 'publicToolTip': 'Public Group', 'privateToolTip': 'Private Group', 'archivedToolTip': 'Archived Group', 'administratorToolTip': 'You administer this group.' }, // Status List item or User List item 'status': { 'replyTo': 'In reply to', 'private': 'Private Message', 'reply': 'Reply', 'share': 'Share', 'favorite': 'Favorite', 'delete': 'Delete', 'confirmDelete': 'Are you sure you want to delete this status?', 'sharedYour': 'shared your status', 'followed': 'followed you', 'shared': 'shared', 'groupAdmin': 'Group Administrator', 'announce': 'Announce', 'isAnnounced': 'Announced by', 'viewConversation': 'View Conversation', 'shares': 'Shares' }, // Tag View 'tag': { 'title': 'Tag', 'follow': 'Follow', 'following': 'Following', 'unfollow': 'Unfollow' }, // Group View 'group': { 'join': 'Join', 'joined': 'Joined', 'leave': 'Leave', 'manage': 'Manage', 'statuses': 'Statuses', 'members': 'Members', 'membersSingular': '1 Member', 'membersPlural': '{{ amount }} Members' }, // Profile View 'profile': { 'statuses': 'Statuses', 'following': 'Following', 'followers': 'Followers', 'follow': 'Follow', 'unfollow': 'Unfollow', 'followsYou': 'Follows You', 'youStatusesSingular': 'Your 1 status', 'youStatusesPlural': 'Your {{ amount }} statuses', 'youFollowingSingular': 'You follow 1 person', 'youFollowingPlural': 'You follow {{ amount }} people', 'youFollowersSingular': 'Your 1 follower', 'youFollowersPlural': 'Your {{ amount }} followers', 'userStatusesSingular': '@{{ username }} posted 1 status', 'userStatusesPlural': '@{{ username }} posted {{ amount }} statuses', 'userFollowingSingular': '@{{ username }} follows 1 person', 'userFollowingPlural': '@{{ username }} follows {{ amount }} people', 'userFollowersSingular': '@{{ username }} has 1 follower', 'userFollowersPlural': '@{{ username }} has {{ amount }} followers', 'deactivatedUser': 'Deactivated User', // Profile Sidebar View 'sidebar': { 'information': 'Information', 'statistics': 'Statistics', 'firstName': 'First Name', 'lastName': 'Last Name', 'email': 'Email', 'jobTitle': 'Job Title', 'phoneNumber': 'Phone Number', 'statuses': 'Statuses', 'following': 'Following', 'followers': 'Followers', 'trends': 'Trends' } }, 'searchPage': { 'title': 'Search', 'statusesWith': 'Statuses with' } }, //Account View 'account': { // Profile Tab 'profile': { 'title': 'Profile', 'dropPhoto': 'Drop your photo here to update it', 'update': 'Update your profile', 'email': 'Email', 'firstName': 'First Name', 'lastName': 'Last Name', 'jobTitle': 'Job Title', 'phoneNumber': 'Phone Number', 'delete': 'Delete your account', 'confirmDelete': 'You are about to delete your account. Are you sure?', 'save': 'Your profile has been saved' }, // Preferences Tab 'preferences': { 'title': 'Preferences', 'notifications': 'Notifications', 'notification': { 'email': { 'mention': 'Get notified by email when you are mentioned', 'dailyDigest': 'Get a daily digest email', 'weeklyDigest': 'Get a weekly digest email' }, 'rss': { 'timeline': 'Allow RSS feed publication of your timeline', 'link': 'Link to your timeline RSS stream' } }, 'save': 'Your preferences have been saved' }, // Password Tab 'password': { 'title': 'Password', 'update': 'Update your password', 'old': 'Old Password', 'new': 'New Password', 'confirm': 'Confirm New Password', 'save': 'Your password has been changed' }, // Files Tab 'files': { 'title': 'Files', 'filename': 'Filename', 'size': 'Size', 'date': 'Date', 'delete': 'Delete' }, // Users Tab 'users': { 'title': 'Users', 'following': 'Following', 'recommended': 'Recommended', 'search': 'Search', 'deactivated': 'This user is deactivated' }, // Groups Tab 'groups': { 'title': 'Groups', 'createNewGroup': 'Create a new group', 'name': 'Name', 'description': 'Description', 'public': 'Public', 'private': 'Private', 'publicWarning': 'Warning: If this group is public, everybody can access it', 'create': 'Create', 'myGroups': 'My Groups', 'recommended': 'Recommended', 'search': 'Search', 'group': 'Group', 'access': 'Access', 'members': 'Members', 'manage': 'Manage', 'update': 'Update group details', 'archive': 'Do you want to archive this group?', 'allowArchive': 'Yes, this group should be archived', 'denyArchive': 'No, this group is still in use', 'archiveWarning': 'Warning: Archived groups are read-only', 'addMember': 'Add a member', 'username': 'Username', 'role': 'Role', 'admin': 'Administrator', 'member': 'Member', 'join': 'Join', 'joined': 'Joined', 'leave': 'Leave', 'archived': 'Archived', 'add': 'Add', 'remove': 'Remove', 'save': 'Your group has been created' }, // Tags Tab 'tags': { 'title': 'Tags', 'trends': 'Trends', 'search': 'Search', 'tag': 'Tag', 'follow': 'Follow', 'following': 'Following', 'unfollow': 'Unfollow' }, // Top Posters Tab 'topPosters': { 'title': 'Top Posters', 'username': 'Username', 'count': 'Status Count' } }, 'form': { 'cancel': 'Cancel', 'save': 'Save', 'success': 'The form has been successfully saved.', 'fail': 'Failed to save form.', 'deleted': 'Your file has been deleted.' }, 'admin': { 'title': 'Administration Dashboard', 'registered': 'Registered Enterprises', 'domain': 'Domain', 'count': '# of users', 'environment': 'Environnement Variables (from tatami.properties)', 'propery': 'Property', 'value': 'Value', 'reindex': 'Re-index Search Engine', 'confirm': 'Are you sure you want to re-index Search Engine?', 'success': 'Search engine re-indexation has succeeded.', 'deactivate': 'Deactivate', 'activate': 'Activate' } } }); $translateProvider.translations('fr', { 'tatami': { 'error': 'Il y a une erreur', 'pageNotFound': 'Page non trouvée.', 'welcome': { 'title': 'Bienvenue sur Tatami', 'message': 'Votre timeline est vide! Avez-vous besoin d\'aide pour apprendre à utilizer Tatami? Veuillez cliquer sur le bouton ci-dessous pour lancer une présentation.', 'presentation': 'Lancer la présentation', 'help': { 'title': 'Aide', 'line1': 'Bienvenue sur l\'aide en ligne!', 'line2':' Suivez les étapes suivantes pour une présentation des principales caractéristiques de Tatami .' }, 'timeline': { 'title': 'Timeline', 'line1': 'Ceci est votre timeline. Il affiche tous les messages', 'bulletPoint1': 'vous mentionner ou envoyé en privé à vous', 'bulletPoint2': 'envoyé par les utilisateurs que vous suivez', 'bulletPoint3': 'envoyé par vous-même ', 'bulletPoint4': 'envoyé à un groupe auquel vous êtes abonné(e)', 'afterBP1': 'Si elle est vide, ne vous inquiétez pas, elle sera mise à jour dès que vous commencez à suivre d\'autres utilisateurs!', 'afterBP2': 'Lors de l\'affichage d\'un message, vous pouvez y répondre et le marquer comme favori pour le retrouver plus facilement.', 'afterBP3': 'Si un message avait déjà obtenu quelques réponses, vous pouvez voir tous les détails en cliquant sur detail : ce sera plus facile de suivre la conversation sur Tatami.' }, 'post': { 'title': 'Envoyer un message', 'line1': 'C\'est ici que vous écrivez les messages que vous souhaitez partager', 'bulletPoint1': 'tous les messages sont publics par défaut. Ils seront affichés à tous les utilisateurs qui vous suivent', 'bulletPoint2': 'pour écrire un message, vous pouvez utiliser de #hashtags : cela signifie tout simplement l\'ajout d\'un «#» au début des mots importants qui peuvent ensuite être utilisé pour trouver votre message', 'bulletPoint3': 'pour mentionner ou répondre à d\'autres utilisateurs, vous devez ajouter un @ au début de leur nom : ils seront informés que vous vous adressez à eux' }, 'groups': { 'title': 'Groupes', 'line1': 'Ceci est la liste des groupes auquel vous appartenez.', 'line2': 'Vous pouvez trouver et vous abonner à des groupe public dans la page Account/Groupes (menu en haut à droite).', 'line3': 'Il y a aussi des groupes privés : vous ne pouvez pas vous y inscrire; le propriétaire du groupe doit vous ajouter en tant que membre.' }, 'trends': { 'title': 'Tendances', 'line1': 'Cette liste représente les #hashtag qui sont les plus souvent utilisés sur Tatami. Utilisez-la pour découvrir ce qui ce passe et quels sont les sujets les plus chauds sur Tatami!' }, 'whoToFollow': { 'title': 'Utilisateurs suggérées', 'line1': 'Ceci est une liste d\'utilisateurs qui partagent des intérêts communs avec vous et que vous pourriez suivre.', 'line2': 'Si vous êtes un nouvel utilisateur, cette liste est probablement vide : Tatami a besoin de temps pour apprendre qui vous êtes afin de vous proposer des utilisateurs', 'line3': 'Et n\'oubliez pas d\'utiliser des #hashtags dans vos messages, cela rend tout plus facile !' }, 'next': 'Suivant', 'previous': 'Précédent', 'end': 'Fin' }, // Login View 'login': { 'title': 'Login', 'mainTitle': 'Bienvenue sur Tatami', 'subtitle': 'Un réseau social d\'entreprise open source', 'moreInfo': 'Plus d\'info', 'email': 'Email', 'password': 'Mot de passe', 'remember': 'Se souvenir de moi', 'forgotPassword': 'Mot de passe oublié ?', 'resetPassword': 'Nouveau mot de passe', 'tos': 'Conditions de service', 'fail': 'Votre authentification a échoué ! Etes-vous sûr que vous avez utilisé un mot de passe correct ?', 'passwordEmailSent': 'Un email vous a été envoyé, avec des instructions pour créer un nouveau mot de passe.', 'unregisteredEmail': 'Cette adresse email n\'est pas enregistrée dans Tatami.', 'register': { 'title': 'Enregistré', 'line1': 'Un email de confirmation sera envoyé à l\'adresse fournie.', 'line2': "Le domaine de votre email détermine l'espace de l'entreprise que vous allez rejoindre. Par exemple, les utilisateurs ayant une adresse email@ippon.fr vont joindre l'espace privé Ippon.", 'line3': "Si vous êtes le premier employé de votre entreprise à se joindre à Tatami, l'espace privé de votre entreprise sera automatiquement créé." }, 'googleApps': { 'title': 'Google Apps Login', 'line1': 'Cette fonction est pour les utilisateurs de Google Apps for Work, qui ont leur nom de domaine de travail géré par Google Apps.', 'link': 'Pour plus d\'informations sur Google Apps for Work, cliquez ici.', 'line2': 'Que vous ayez déjà ou pas un compte, vous pouvez vous connecter avec votre compte Google Apps.', 'line3': "Votre email sera fourni par Google et le nom du domaine de votre email sera utilisé pour vous permettre de rejoindre l'espace privé de votre entreprise.", 'login': 'Login avec Google Apps' }, 'validation': 'Validation email', 'passwordSuccess': 'Votre email a été validé. Votre mot de passe vous sera envoyé par email.', 'returnHome': 'Allez à la page d\'accueil', 'registrationEmail': 'Merci ! Un e-mail de confirmation vous a été envoyé.' }, 'about': { // Presentation Page 'presentation': { 'title': 'Qu\'est-ce que Tatami ?', 'devices': 'Appareils', 'openSource': 'Open Source', 'row1': { 'title': 'Un réseau social d\'entreprise privé', 'line1': 'Mettre à jour votre statut afin d\'informer vos collègues', 'line2': "S'abonner aux timelines des autres employés", 'line3': 'Partager des informations importantes aux personnes qui vous suivent', 'line4': 'Discuter et répondre à vos collègues', 'line5': 'Ajouter des informations importantes dans vos favoris', 'line6': 'Chercher des informations utiles avec notre moteur de recherche intégré', 'line7': 'Utiliser les #hashtags lors de votre recherche', 'line8': "Aller voir le profil de vos collègues et découvrez ce sur quoi ils travaillent", 'line9': 'Tatami est disponible en français et en anglais. Il est facile d\'ajouter d\'autres langues' }, 'row2': { 'title': 'Fonctionne partout !', 'line1': 'Application web dynamique (HTML5): rien à installer, un navigateur moderne suffit !', 'line2': "Fonctionne sur les appareils mobiles, des tablettes ou des ordinateurs: l'application s'adapte automatiquement à l'écran de votre appareil", 'line3': 'Restez connecté avec votre entreprise où que vous soyez' }, 'row3': { 'title': "Installation et intégration facile avec l'infrastructure informatique de votre entreprise", 'line1': 'Application standard Java', 'line2': 'Vos données vous appartiennent et ne sont pas à votre fournisseur SaaS !', 'line3': 'Intégration avec votre dossier LDAP', 'line4': 'Intégration avec Google Apps', 'line5': 'Entièrement Open Source, avec une licence Apache 2.0, facile à utiliser pour les entreprises', 'line6': 'Facile à étendre ou modifier selon vos besoins', 'line7': 'Haute performance (basé sur Cassandra d\'Apache), même sur les petites configurations', 'line8': 'Rejoignez le projet et proposer des patches sur notre page Github:' }, 'row4': { 'title': "Egalement disponible en mode SaaS, entièrement géré par Ippon Technologies", 'line1': "Si vous ne souhaitez pas installer Tatami dans votre entreprise, il est facile de l'utiliser directement", 'line2': 'Mode multi-entreprise sécurisé : chaque entreprise dispose de son propre espace privé', 'line3': 'Cryptage SSL 256 bits : tous les transferts de données sont entièrement sécurisé' }, 'row5': { 'title': 'Besoin de plus d\'informations sur notre produit ?', 'line1': 'Notre équipe de vente est impatiente de vous entendre! Appelez-nous au +33 1 46 12 48 48 ou par email à' } }, // Terms of Service Page 'tos': { 'title': 'Conditions de service' }, // License Page 'license': { 'title': 'Licence du code source', 'copyright': 'Droits d\'auteur' } }, // Home View 'home': { 'timeline': 'Timeline', 'mentions': 'Mentions', 'favorites': 'Favoris', 'companyTimeline': 'Timeline de votre entreprise', 'newMessage': 'Nouveau Message', 'newMessages': 'Nouveaux Messages', //Top Menu 'menu': { 'logo': 'Ippon Technologies Logo', 'title': 'Tatami', 'about': { 'title': 'Information', 'version' : 'Version', 'presentation': 'Présentation', 'tos': 'Conditions générales d\'utilisation', 'language': { 'language': 'Language', 'en': 'English', 'fr': 'Français' }, 'license': 'Licence du source code', 'github': { 'issues': 'Envoyer un rapport de bug', 'fork': 'Fork Tatami sur Github' }, 'ippon': { 'website': 'Site d\'Ippon Technologies', 'blog': 'Blog d\'Ippon Technologies', 'twitter': 'Suivez @ippontech sur Twitter' } }, 'search': 'Recherchez', 'help': 'Aide', 'account': { 'title': 'Compte', 'companyTimeline': 'Timeline de votre enterprise', 'logout': 'Déconnexion' } }, // Post Modal 'post': { 'mandatory': 'Commentaire obligatoire', 'content': { 'mandatory': 'Veuillez remplir ce champ..' }, 'update': 'Mettre à jour votre statut', 'replyTo': 'Répondre', 'preview': 'Prévisualisation', 'edit': 'Éditer', 'characters': { 'left': 'Caractères restants :' }, 'options': 'Options', 'shareLocation': 'Partager votre localisation', 'group': 'Groupe', 'reply': 'Répondre à ce statut', 'files': 'Fichiers', 'drop': { 'file': 'Déposez vos fichiers ici' }, 'markdown': 'Markdown Supported', 'button': 'Post' }, // Home Sidebar View 'sidebar': { 'myGroups': 'Mes Groupes', 'whoToFollow': 'Qui Suivre', 'trends': 'Tendances', 'public': 'PUB', 'private': 'PVT', 'archived': 'ARC', 'administrator': 'A', 'publicToolTip': 'Groupe public', 'privateToolTip': 'Groupe privé', 'archivedToolTip': 'Groupe archivé', 'administratorToolTip': 'Vous administrez ce groupe.' }, // Status List item or User List item 'status': { 'replyTo': 'En réponse à', 'private': 'Message privé', 'reply': 'Répondre', 'share': 'Partager', 'favorite': 'Favoris', 'delete': 'Supprimer', 'confirmDelete': 'Êtes-vous sûr de vouloir supprimer ce statut ?', 'sharedYour': 'a partagé votre statut', 'followed': 'vous suit', 'shared': 'partagé', 'groupAdmin': 'Administrateur du groupe', 'announce': 'Annoncer', 'isAnnounced': 'Annoncé par', 'viewConversation': 'Voir La Conversation', 'shares': 'Partages' }, // Tag View 'tag': { 'title': 'Tag', 'follow': 'Abonnés', 'following': 'Abonnements', 'unfollow': 'Se désabonner' }, // Group View 'group': { 'join': 'Joindre', 'joined': 'a rejoins', 'leave': 'Quitter', 'manage': 'Gérer', 'statuses': 'Statuts', 'members': 'Membres', 'membersSingular': '1 Membre', 'membersPlural': '{{ amount }} Membres' }, // Profile View 'profile': { 'statuses': 'Statuts', 'following': 'Abonnement', 'followers': 'Abonnés', 'follow': 'Suivre', 'unfollow': 'Se désabonner', 'followsYou': 'Vous suit', 'youStatusesSingular': 'Votre statut', 'youStatusesPlural': 'Vos {{ amount }} statuts', 'youFollowingSingular': 'Vous êtes abonné(e) à 1 personne', 'youFollowingPlural': 'Vous êtes abonné(e) à {{ amount }} personnes', 'youFollowersSingular': '1 personne vous suit', 'youFollowersPlural': '{{ amount }} personnes vous suivent', 'userStatusesSingular': '@{{ username }} a partagé un statut', 'userStatusesPlural': '@{{ username }} a partagé {{ amount }} statuts', 'userFollowingSingular': '@{{ username }} suit 1 personne', 'userFollowingPlural': '@{{ username }} suit {{ amount }} personnes', 'userFollowersSingular': '@{{ username }} a 1 abonnement', 'userFollowersPlural': '@{{ username }} a {{ amount }} abonnements', 'deactivatedUser': 'Utilisateur désactivé', // Profile Sidebar View 'sidebar': { 'information': 'Information', 'statistics': 'Statistiques', 'firstName': 'Prénom', 'lastName': 'Nom', 'email': 'Email', 'jobTitle': 'Titre', 'phoneNumber': 'Numéro de téléphone', 'statuses': 'Statuts', 'following': 'Abonnement', 'followers': 'Abonné', 'trends': 'Tendances' } }, 'searchPage': { 'title': 'Recherchez', 'statusesWith': 'Statuts avec' } }, //Account View 'account': { // Profile Tab 'profile': { 'title': 'Profil', 'dropPhoto': 'Déposez votre photo ici pour la mettre à jour', 'update': 'Mettez à jour votre profil', 'email': 'Email', 'firstName': 'Prénom', 'lastName': 'Nom', 'jobTitle': 'Intitulé de poste', 'phoneNumber': 'Numéro de téléphone', 'delete': 'Supprimer votre compte', 'confirmDelete': 'Vous êtes sur le point de supprimer votre compte. Êtes-vous sûr?', 'save': 'Votre profil a été sauvé' }, // Preferences Tab 'preferences': { 'title': 'Préférences', 'notifications': 'Notifications', 'notification': { 'email': { 'mention': 'Recevez une notification par email lorsque vous êtes mentionné', 'dailyDigest': 'Obtenez un résumé quotidien email', 'weeklyDigest': 'Obtenez un sommaire hebdomadaire email' }, 'rss': { 'timeline': 'Autoriser la publication RSS de votre fil d\'actualité', 'link': 'Lien de votre RSS fils d\'actualité' } }, 'save': 'Vos préférences ont été enregistrées' }, // Password Tab 'password': { 'title': 'Mot de passe', 'update': 'Changez votre mot de passe', 'old': 'Ancien mot de passe', 'new': 'Nouveau mot de passe', 'confirm': 'Confirmer votre nouveau mot de passe', 'save': 'Votre mot de passe a été changé' }, // Files Tab 'files': { 'title': 'Fichier', 'filename': 'Nom de fichier', 'size': 'Taille', 'date': 'Date', 'delete': 'Supprimer' }, // Users Tab 'users': { 'title': 'Utilisateur', 'following': 'Abonné', 'recommended': 'Recommandé', 'search': 'Rechercher', 'deactivated': 'Cet utilisateur est désactivé' }, // Groups Tab 'groups': { 'title': 'Groupes', 'createNewGroup': 'Créer un nouveau groupe', 'name': 'Nom', 'description': 'Description', 'public': 'Public', 'private': 'Privé', 'publicWarning': 'Attention: Si ce groupe est public, tout le monde peut y accéder', 'create': 'Créer', 'myGroups': 'Mes Groupes', 'recommended': 'Recommandé', 'search': 'Rechercher', 'group': 'Groupe', 'access': 'Accès', 'members': 'Membres', 'manage': 'Gérer', 'update': 'Mettre à jour les détails de votre groupe', 'archive': 'Voulez-vous archiver ce groupe?', 'allowArchive': 'Oui, ce groupe devrait être archivé', 'denyArchive': 'Non, ce groupe est encore utilisé', 'archiveWarning': 'Attention : les groupe archivés sont en lecture seule', 'addMember': 'Ajouter un membre', 'username': 'Username', 'role': 'Rôle', 'admin': 'Administrator', 'member': 'Membre', 'join': 'Joindre', 'joined': 'Rejoint', 'leave': 'Quitter', 'archived': 'Archivé', 'add': 'Ajouter', 'remove': 'Supprimer', 'save': 'Votre groupe a été créé' }, // Tags Tab 'tags': { 'title': 'Tags', 'trends': 'Tendances', 'search': 'Rechercher', 'tag': 'Tag', 'follow': 'Abonné', 'following': 'Abonnement', 'unfollow': 'Se désabonner' }, // Top Posters Tab 'topPosters': { 'title': 'Top Posters', 'username': 'Nom', 'count': 'Nombre de Statuts' } }, 'form': { 'cancel': 'Annuler', 'save': 'Sauvegarder', 'success': 'Le formulaire a été enregistré avec succès.', 'fail': 'Impossible de sauvegarder le formulaire.', 'deleted': 'Votre fichier a été supprimé.' }, 'admin': { 'title': 'Dashboard d\'administration', 'registered': 'Entreprises enregistrées', 'domain': 'Domaine', 'count': '# d\'utilisateur(s)', 'environment': 'Variables d\'environnement (de tatami.properties)', 'propery': 'Property', 'value': 'Valeur', 'reindex': 'Ré-indexation du moteur de recherche', 'confirm': 'Êtes-vous sûr que vous voulez ré-indexer les moteurs de recherche?', 'success': 'La ré-indexation du moteur de recherche a réussi.', 'deactivate': 'Désactiver', 'activate': 'Activer' } } }); $translateProvider.useCookieStorage(); $translateProvider.registerAvailableLanguageKeys(['en', 'fr']); $translateProvider.fallbackLanguage('en'); $translateProvider.determinePreferredLanguage(); }]); ================================================ FILE: web/src/main/webapp/app/shared/error/404View.html ================================================

    ================================================ FILE: web/src/main/webapp/app/shared/error/500View.html ================================================

    ================================================ FILE: web/src/main/webapp/app/shared/filters/EmoticonFilter.js ================================================ TatamiApp.filter('emoticon', function() { return function(content) { if(content === null || angular.isUndefined(content)) { return content; } var emoticons = { '>:(': '/assets/img/emoticons/angry.png', ':$': '/assets/img/emoticons/blushing.png', '8)': '/assets/img/emoticons/cool.png', 'B)': '/assets/img/emoticons/cool.png', ":'(": '/assets/img/emoticons/crying.png', ':(': '/assets/img/emoticons/frowning.png', ':o': '/assets/img/emoticons/gasping.png', ':O': '/assets/img/emoticons/gasping.png', ':D': '/assets/img/emoticons/grinning.png', '<3': '/assets/img/emoticons/heart.png', 'XD': '/assets/img/emoticons/laughing.png', ':x': '/assets/img/emoticons/lips_sealed.png', ':X': '/assets/img/emoticons/lips_sealed.png', ':#': '/assets/img/emoticons/lips_sealed.png', '>:D': '/assets/img/emoticons/malicious.png', ':3': '/assets/img/emoticons/naww.png', ':)': '/assets/img/emoticons/smiling.png', ':|': '/assets/img/emoticons/speechless.png', '>:)': '/assets/img/emoticons/spiteful.png', 'o_O': '/assets/img/emoticons/surprised.png', 'D:': '/assets/img/emoticons/terrified.png', ':-1:': '/assets/img/emoticons/thumbs_down.png', ':+1:': '/assets/img/emoticons/thumbs_up.png', 'XP': '/assets/img/emoticons/tongue_out_laughing.png', ':p': '/assets/img/emoticons/tongue_out.png', ':P': '/assets/img/emoticons/tongue_out.png', ':/': '/assets/img/emoticons/unsure.png', ';)': '/assets/img/emoticons/winking_grinning.png', ';p': '/assets/img/emoticons/winking_tongue_out.png', ';P': '/assets/img/emoticons/winking_tongue_out.png', ':t': '/assets/img/emoticons/trollface.png', ':T': '/assets/img/emoticons/trollface.png', '(troll)': '/assets/img/emoticons/trollface.png' }; function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } for(var key in emoticons) { var reg = new RegExp('(^| )' + escapeRegExp(key) + '($| )'); content = content.replace(reg, ' ![' + key + '](' + emoticons[key] + ') '); } return content; }; }); ================================================ FILE: web/src/main/webapp/app/shared/filters/MarkdownFilter.js ================================================ TatamiApp.filter('markdown', ['$sce', function($sce) { return function(content) { return content ? $sce.trustAsHtml(marked(content)) : ''; }; }]); ================================================ FILE: web/src/main/webapp/app/shared/filters/PlaceholderFilter.js ================================================ TatamiApp.filter('placeholderFilter', ['$translate', function($translate) { return function(isReply) { if(isReply) { return ""; } else { return $translate.instant('tatami.home.post.update'); } }; }]); ================================================ FILE: web/src/main/webapp/app/shared/footer/FooterController.js ================================================ FooterModule.controller('FooterController', ['$scope', function($scope) { }]); ================================================ FILE: web/src/main/webapp/app/shared/footer/FooterModule.js ================================================ var FooterModule = angular.module('FooterModule', []); ================================================ FILE: web/src/main/webapp/app/shared/footer/FooterView.html ================================================
    Version: version, Build: build
    ================================================ FILE: web/src/main/webapp/app/shared/lists/status/withContext/StatusListContextController.js ================================================ HomeModule.controller('StatusListContextController', [ '$scope', '$q', '$timeout', '$window', '$translate', 'StatusService', 'HomeService', 'TagService', 'GroupService', 'profile', 'statuses', 'statusesWithContext', 'userRoles', 'showModal', function($scope, $q, $timeout, $window, $translate, StatusService, HomeService, TagService, GroupService, profile, statuses, statusesWithContext, userRoles, showModal) { if(showModal && $scope.$state.is('tatami.home.home.timeline')) { $scope.$state.go('tatami.home.home.timeline.presentation'); } $scope.isAdmin = userRoles.roles.indexOf('ROLE_ADMIN') !== -1; $scope.profile = profile; $scope.statusesWithContext = statusesWithContext; $scope.busy = false; if(statuses.length === 0) { $scope.end = true; } else { $scope.end = false; $scope.finish = statuses[statuses.length - 1].timelineId; } /* Begin Polling Related Code */ $scope.newMessages = null; $window.document.title = 'Tatami'; if($scope.$state.is('tatami.home.home.timeline') || $scope.$state.is('tatami.home.home.company')) { var requestNewStatuses = function() { // In milliseconds var pollingDelay = 20000; $scope.poller = $timeout(function() { var args = {}; if($scope.statuses.length !== 0) { args = { start: statuses[0].timelineId }; } var success = function(response) { if(response.length > 0) { $scope.newMessages = response.length; $window.document.title = 'Tatami (' + $scope.newMessages + ')'; } requestNewStatuses(); }; var error = function() { requestNewStatuses(); }; if($scope.$state.is('tatami.home.home.timeline')) { StatusService.getHomeTimeline(args, success, error); } else if($scope.$state.is('tatami.home.home.company')) { HomeService.getCompanyTimeline(args, success, error); } }, pollingDelay); }; requestNewStatuses(); $scope.$on('$destroy', function() { $timeout.cancel($scope.poller); }); } /* End Polling Related Code */ /* Begin Infinite Scrolling Related Code */ var organizeStatuses = function(statuses, context) { var statusesWithContext = []; // Fill array with context statuses for(i = 0; i < context.length; i++) { if(context[i] !== null) { statusesWithContext.push({ status: context[i], replies: [] }); } } var individualStatuses = []; // Attach replies to corresponding context status for(i = 0; i < statuses.length; i++) { if(statuses[i].replyTo) { for(j = 0; j < statusesWithContext.length; j++) { if(statuses[i].replyTo === statusesWithContext[j].status.statusId) { statusesWithContext[j].replies.unshift(statuses[i]); break; } // If the context reply doesn't exist, then make the reply an individual status if(j === statusesWithContext.length - 1) { individualStatuses.push(statuses[i]); break; } } } else { var addIt = true; // Check current timeline for(j = 0; j < $scope.statusesWithContext.length; j++) { // If the status isn't already in the timeline as a // context status, then add it to individualStatuses if(statuses[i].statusId === $scope.statusesWithContext[j].status.statusId) { addIt = false; break; } } // Check new timeline chunk for(j = 0; j < statusesWithContext.length; j++) { // If the status isn't already in the timeline as a // context status, then add it to individualStatuses if(statuses[i].statusId === statusesWithContext[j].status.statusId) { addIt = false; break; } } if(addIt) { individualStatuses.push(statuses[i]); } } } // Put remaining individual statuses (ones that aren't replies) into the timeline for(i = 0; i < individualStatuses.length; i++) { // If the timeline is empty, put in a status if(statusesWithContext.length === 0) { statusesWithContext.push({ status: individualStatuses[i], replies: null }); continue; } for(var j = 0; j <= statusesWithContext.length; j++) { try { // If the status block has replies, we need to check the // last reply's post date/time, because that is the latest status in the block. // We order the timeline by the latest status in the block. if(statusesWithContext[j].replies !== null && statusesWithContext[j].replies.length !== 0) { var index = statusesWithContext[j].replies.length - 1; if(statusesWithContext[j].replies[index].statusDate < individualStatuses[i].statusDate) { statusesWithContext.splice(j, 0, { status: individualStatuses[i], replies: null }); break; } } else { // Otherwise compare using the date of the individual status if(statusesWithContext[j].status.statusDate < individualStatuses[i].statusDate) { statusesWithContext.splice(j, 0, { status: individualStatuses[i], replies: null }); break; } } } catch(err) { // For statuses that are at the end (bottom) of the timeline statusesWithContext.push({ status: individualStatuses[i], replies: null }); break; } } } for(var i = 0; i < statusesWithContext.length; i++) { $scope.statusesWithContext.push(statusesWithContext[i]); } $scope.finish = statuses[statuses.length - 1].timelineId; $scope.busy = false; }; var getContext = function(statuses) { if(statuses.length === 0) { // reached end of list $scope.end = true; return; } var temp = new Set(); var context = []; for(var i = 0; i < statuses.length; i++) { if(statuses[i].replyTo && !temp.has(statuses[i].replyTo)) { temp.add(statuses[i].replyTo); context.push(StatusService.get({ statusId: statuses[i].replyTo }) .$promise.then( function(response) { if(angular.equals({}, response.toJSON())) { return $q.when(null); } return response; })); } } $q.all(context).then(function(context) { organizeStatuses(statuses, context); }); }; $scope.requestOldStatuses = function() { if($scope.busy || $scope.end) { return; } $scope.busy = true; if($scope.$state.is('tatami.home.home.timeline')) { StatusService.getHomeTimeline({ finish: $scope.finish }, getContext); } else if($scope.$state.is('tatami.home.home.company')) { HomeService.getCompanyTimeline({ finish: $scope.finish }, getContext); } }; /* End Infinite Scrolling Related Code */ $scope.isOneDayOrMore = function(date) { return moment().diff(moment(date), 'days', true) >= 1; }; $scope.getLanguageKey = function() { return $translate.use(); }; $scope.openReplyModal = function(status) { $scope.$state.go($scope.$state.current.name + '.post', { statusId: status.statusId }); }; $scope.favoriteStatus = function(status) { StatusService.update({ statusId: status.statusId }, { favorite: !status.favorite }, function() { $scope.$state.reload(); }); }; $scope.shareStatus = function(status) { StatusService.update({ statusId: status.statusId }, { shared: !status.shareByMe }, function() { $scope.$state.reload(); }); }; $scope.announceStatus = function(status) { StatusService.update({ statusId: status.statusId }, { announced: true }, function() { $scope.$state.reload(); }); }; $scope.deleteStatus = function(status) { // Need a confirmation modal here StatusService.delete({ statusId: status.statusId }, null, function() { $scope.$state.reload(); }); }; $scope.getShares = function(status, firstIndex, secondIndex) { if(status.type === 'STATUS' && status.shares === null) { StatusService.getDetails({ statusId: status.statusId }, null, function(response) { if(secondIndex === null) { $scope.statusesWithContext[firstIndex].status.shares = response.sharedByLogins; } else { $scope.statusesWithContext[firstIndex]['replies'][secondIndex].shares = response.sharedByLogins; } }); } }; } ]); ================================================ FILE: web/src/main/webapp/app/shared/lists/status/withContext/StatusListContextView.html ================================================
    {{ newMessages }}
    ================================================ FILE: web/src/main/webapp/app/shared/lists/status/withoutContext/StatusListController.js ================================================ HomeModule.controller('StatusListController', [ '$scope', '$timeout', '$window', '$translate', 'StatusService', 'HomeService', 'TagService', 'GroupService', 'profile', 'statuses', 'userRoles', 'showModal', function($scope, $timeout, $window, $translate, StatusService, HomeService, TagService, GroupService, profile, statuses, userRoles, showModal) { if(showModal && $scope.$state.is('tatami.home.home.timeline')) { $scope.$state.go('tatami.home.home.timeline.presentation'); } $scope.isAdmin = userRoles.roles.indexOf('ROLE_ADMIN') !== -1; $scope.profile = profile; $scope.statuses = statuses; $scope.busy = false; if($scope.statuses.length === 0) { $scope.end = true; } else { $scope.end = false; $scope.finish = $scope.statuses[$scope.statuses.length - 1].timelineId; } /* Begin Polling Related Code */ $scope.newMessages = null; $window.document.title = 'Tatami'; var requestNewStatuses = function() { // In milliseconds var pollingDelay = 20000; $scope.poller = $timeout(function() { var args = null; if($scope.statuses.length > 0) { if($scope.$state.is('tatami.home.home.tag')) { args = { tag: $scope.$stateParams.tag, start: statuses[0].timelineId }; } else if($scope.$state.is('tatami.home.home.group.statuses')) { args = { groupId: $scope.$stateParams.groupId, start: statuses[0].timelineId }; } else if($scope.$state.is('tatami.home.profile.statuses')) { args = { username: $scope.$stateParams.username, start: statuses[0].timelineId }; } else { args = { start: statuses[0].timelineId }; } } else { if($scope.$state.is('tatami.home.home.tag')) { args = { tag: $scope.$stateParams.tag }; } else if($scope.$state.is('tatami.home.home.group.statuses')) { args = { groupId: $scope.$stateParams.groupId }; } else if($scope.$state.is('tatami.home.profile.statuses')) { args = { username: $scope.$stateParams.username }; } else { args = {}; } } var success = function(response) { if(response.length > 0) { $scope.newMessages = response; $window.document.title = 'Tatami (' + $scope.newMessages.length + ')'; } requestNewStatuses(); }; var error = function() { requestNewStatuses(); }; if($scope.$state.is('tatami.home.home.timeline')) { StatusService.getHomeTimeline(args, success, error); } else if($scope.$state.is('tatami.home.home.mentions')) { HomeService.getMentions(args, success, error); } else if($scope.$state.is('tatami.home.home.company')) { HomeService.getCompanyTimeline(args, success, error); } else if($scope.$state.is('tatami.home.home.tag')) { TagService.getTagTimeline(args, success, error); } else if($scope.$state.is('tatami.home.home.group.statuses')) { GroupService.getStatuses(args, success, error); } else if($scope.$state.is('tatami.home.profile.statuses')) { StatusService.getUserTimeline(args, success, error); } }, pollingDelay); }; requestNewStatuses(); $scope.$on('$destroy', function() { $timeout.cancel($scope.poller); }); $scope.loadNewStatuses = function() { for(var i = $scope.newMessages.length - 1; i >= 0 ; i--) { $scope.statuses.unshift($scope.newMessages[i]); } $scope.newMessages = null; $window.document.title = 'Tatami'; }; /* End Polling Related Code */ /* Begin Infinite Scrolling Related Code */ $scope.requestOldStatuses = function() { if($scope.busy || $scope.end) { return; } $scope.busy = true; if($scope.$state.is('tatami.home.home.timeline')) { StatusService.getHomeTimeline({ finish: $scope.finish }, loadOldStatuses); } else if($scope.$state.is('tatami.home.home.company')) { HomeService.getCompanyTimeline({ finish: $scope.finish }, loadOldStatuses); } else if($scope.$state.is('tatami.home.home.mentions')) { HomeService.getMentions({ finish: $scope.finish }, loadOldStatuses); } /* Favorites are limited to 50 total per user. All 50 are loaded from the favorites REST endpoint at once. There is no way to use &finish=timelineId for favorites as of now in the backend. Keep this commented out until the backend is changed to allow for more than 50 favorites and adding &finish=timelineId to the REST url. */ else if($scope.$state.is('tatami.home.home.tag')) { TagService.getTagTimeline({ tag: $scope.$stateParams.tag, finish: $scope.finish }, loadOldStatuses); } else if($scope.$state.is('tatami.home.home.group.statuses')) { GroupService.getStatuses({ groupId: $scope.$stateParams.groupId, finish: $scope.finish }, loadOldStatuses); } else if($scope.$state.is('tatami.home.profile.statuses')) { StatusService.getUserTimeline({ username: $scope.$stateParams.username, finish: $scope.finish }, loadOldStatuses); } }; var loadOldStatuses = function(statuses) { if(statuses.length === 0){ $scope.end = true; // reached end of list return; } for(var i = 0; i < statuses.length; i++) { $scope.statuses.push(statuses[i]); } $scope.finish = $scope.statuses[$scope.statuses.length - 1].timelineId; $scope.busy = false; }; /* End Infinite Scrolling Related Code */ $scope.isOneDayOrMore = function(date) { return moment().diff(moment(date), 'days', true) >= 1; }; $scope.getLanguageKey = function() { return $translate.use(); }; $scope.openReplyModal = function(status) { $scope.$state.go($scope.$state.current.name + '.post', { statusId: status.statusId }); }; $scope.favoriteStatus = function(status, index) { StatusService.update({ statusId: status.statusId }, { favorite: !status.favorite }, function(response) { $scope.statuses[index].favorite = response.favorite; }); }; $scope.shareStatus = function(status, index) { StatusService.update({ statusId: status.statusId }, { shared: !status.shareByMe }, function(response) { $scope.statuses[index].shareByMe = response.shareByMe; }); }; $scope.announceStatus = function(status) { StatusService.update({ statusId: status.statusId }, { announced: true }, function() { $scope.$state.reload(); }); }; $scope.deleteStatus = function(status, index) { // Need a confirmation modal here StatusService.delete({ statusId: status.statusId }, null, function() { $scope.statuses.splice(index, 1); }); }; $scope.getShares = function(status, index) { if(status.type === 'STATUS' && status.shares === null) { StatusService.getDetails({ statusId: status.statusId }, null, function(response) { $scope.statuses[index].shares = response.sharedByLogins; }); } }; } ]); ================================================ FILE: web/src/main/webapp/app/shared/lists/status/withoutContext/StatusListView.html ================================================
    {{ newMessages.length }}
    ================================================ FILE: web/src/main/webapp/app/shared/lists/user/UserListController.js ================================================ HomeModule.controller('UserListController', ['$scope', 'UserService', 'users', function($scope, UserService, users) { $scope.users = users; $scope.followUser = function(user, index) { UserService.follow({ username: user.username }, { friend: !user.friend, friendShip: true }, function(response) { $scope.users[index].friend = response.friend; $scope.$state.reload(); }); }; } ]); ================================================ FILE: web/src/main/webapp/app/shared/lists/user/UserListView.html ================================================
    ================================================ FILE: web/src/main/webapp/app/shared/services/AuthenticationService.js ================================================ TatamiApp.factory('AuthenticationService', ['$rootScope', '$state', 'UserSession', function($rootScope, $state, UserSession) { return { authenticate: function() { return UserSession.authenticate().then(function(result) { if(result !== null && result.action === null && $state.current.data && !$state.current.data.public) { // User isn't login in. Change the session token, and redirect to login UserSession.clearSession(); $state.go('tatami.login.main'); } }); } } }]); ================================================ FILE: web/src/main/webapp/app/shared/services/GeolocService.js ================================================ /** * This service is used to handle getting the geolocalisation of a user */ TatamiApp.factory('GeolocalisationService', function() { return { /** * Uses HTML5 to get geolocation information from the user * @returns If geolocation data can be found for the user * then we return the position in the form * "lat, lon" * Otherwise, we return the empty string. */ getGeolocalisation: function(callback) { if(navigator.geolocation){ navigator.geolocation.getCurrentPosition(function(position) { callback(position); }); } }, /** * Returns a string with the location data based on the openstreetmap website * @param position The users position * @returns {string} The URL to openstreetmaps */ getGeolocUrl: function(position) { var latitude = position.split(',')[0].trim(); var longitude = position.split(',')[1].trim(); return 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } }); ================================================ FILE: web/src/main/webapp/app/shared/services/GroupService.js ================================================ TatamiApp.factory('GroupService', ['$resource', function($resource) { return $resource('/tatami/rest/groups/:groupId', null, { 'getStatuses': { method: 'GET', isArray: true, params: { groupId: '@groupId' }, url: '/tatami/rest/groups/:groupId/timeline', transformResponse: function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = statuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + statuses[i].avatar + '/photo.jpg'; if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; } }, 'getMembers': { method: 'GET', isArray: true, params: { groupId: '@groupId' }, url: '/tatami/rest/groups/:groupId/members/', transformResponse: function(users) { users = angular.fromJson(users); for(var i = 0; i < users.length; i++) { users[i]['avatarURL'] = users[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + users[i].avatar + '/photo.jpg'; } return users; } }, 'getRecommendations': { method: 'GET', isArray: true, url: '/tatami/rest/groupmemberships/suggestions' }, 'join': { method: 'PUT', params: { groupId: '@groupId', username: '@username' }, url: '/tatami/rest/groups/:groupId/members/:username' }, 'leave': { method: 'DELETE', params: { groupId: '@groupId', username: '@username' }, url: '/tatami/rest/groups/:groupId/members/:username' }, 'update': { method: 'PUT', params: { groupId: '@groupId' }, url: '/tatami/rest/groups/:groupId' } }); }]); ================================================ FILE: web/src/main/webapp/app/shared/services/HomeService.js ================================================ TatamiApp.factory('HomeService', ['$resource', function($resource) { var responseTransform = function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = statuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + statuses[i].avatar + '/photo.jpg'; if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; }; return $resource(null, null, { 'getMentions': { method: 'GET', isArray: true, url: '/tatami/rest/mentions', transformResponse: responseTransform }, 'getFavorites': { method: 'GET', isArray: true, url: '/tatami/rest/favorites', transformResponse: responseTransform }, 'getCompanyTimeline': { method: 'GET', isArray: true, url: '/tatami/rest/company', transformResponse: responseTransform } }); }]); ================================================ FILE: web/src/main/webapp/app/shared/services/ProfileService.js ================================================ angular.module('TatamiApp.services', []) .factory('ProfileService', ['$resource', function ($resource) { return $resource('/tatami/rest/account/profile', null, { 'get': { method: 'GET', transformResponse: function (profile) { var parsedProfile = {}; try { parsedProfile = angular.fromJson(profile); } catch(e) { parsedProfile = {}; } parsedProfile['avatarURL'] = parsedProfile.avatar === '' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + parsedProfile.avatar + '/photo.jpg'; return parsedProfile; } }, 'update': { method: 'PUT', transformRequest: function (profile) { delete profile['avatarURL']; return angular.toJson(profile); } } }); }]); ================================================ FILE: web/src/main/webapp/app/shared/services/SearchService.js ================================================ TatamiApp.factory('SearchService', ['$resource', function($resource) { var responseTransform = function(users) { users = angular.fromJson(users); for(var i = 0; i < users.length; i++) { users[i]['avatarURL'] = users[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + users[i].avatar + '/photo.jpg'; } return users; }; return $resource('/tatami/rest/search/:term', { term: '@term' }, { 'query': { method: 'GET', isArray: true, transformResponse: responseTransform }, 'get': { method: 'GET', transformResponse: responseTransform } }); }]); ================================================ FILE: web/src/main/webapp/app/shared/services/StatusService.js ================================================ TatamiApp.factory('StatusService', ['$resource', function($resource) { var responseTransform = function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = statuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + statuses[i].avatar + '/photo.jpg'; if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; }; return $resource('/tatami/rest/statuses/:statusId', null, { 'get': { method: 'GET', transformResponse: function(status) { status = angular.fromJson(status); status.avatarURL = status.avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + status.avatar + '/photo.jpg'; if(status.geoLocalization) { var latitude = status.geoLocalization.split(',')[0].trim(); var longitude = status.geoLocalization.split(',')[1].trim(); status['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } return status; } }, 'getHomeTimeline': { method: 'GET', isArray: true, url: '/tatami/rest/statuses/home_timeline', transformResponse: responseTransform }, 'getUserTimeline': { method: 'GET', isArray: true, params: { username: '@username' }, url: '/tatami/rest/statuses/:username/timeline', transformResponse: responseTransform }, 'getDetails': { method: 'GET', params: { statusId: '@statusId' }, url: '/tatami/rest/statuses/details/:statusId', transformResponse: function(details) { details = angular.fromJson(details); for(var i = 0; i < details.discussionStatuses.length; i++) { details.discussionStatuses[i]['avatarURL'] = details.discussionStatuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + details.discussionStatuses[i].avatar + '/photo.jpg'; if(details.discussionStatuses[i].geoLocalization) { var latitude = details.discussionStatuses[i].geoLocalization.split(',')[0].trim(); var longitude = details.discussionStatuses[i].geoLocalization.split(',')[1].trim(); details.discussionStatuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } for(var i = 0; i < details.sharedByLogins.length; i++) { details.sharedByLogins[i]['avatarURL'] = details.sharedByLogins[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + details.sharedByLogins[i].avatar + '/photo.jpg'; } return details; } }, 'update': { method: 'PATCH', params: { statusId: '@statusId' } }, 'announce': { method: 'PATCH', params: { params: '@statusId' } } }); }]); ================================================ FILE: web/src/main/webapp/app/shared/services/TagService.js ================================================ TatamiApp.factory('TagService', ['$resource', function($resource) { return $resource('/tatami/rest/tags', null, { 'get': { method:'GET', params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag' }, 'getTagTimeline': { method:'GET', isArray: true, params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag/tag_timeline', transformResponse: function(statuses) { statuses = angular.fromJson(statuses); for(var i = 0; i < statuses.length; i++) { statuses[i]['avatarURL'] = statuses[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + statuses[i].avatar + '/photo.jpg'; if(statuses[i].geoLocalization) { var latitude = statuses[i].geoLocalization.split(',')[0].trim(); var longitude = statuses[i].geoLocalization.split(',')[1].trim(); statuses[i]['locationURL'] = 'https://www.openstreetmap.org/?mlon=' + longitude + '&mlat=' + latitude; } } return statuses; } }, 'follow': { method:'PUT', params: { tag: '@tag' }, url: '/tatami/rest/tags/:tag' }, 'getPopular': { method: 'GET', isArray: true, url: '/tatami/rest/tags/popular' } }); }]); ================================================ FILE: web/src/main/webapp/app/shared/services/TopPostersService.js ================================================ TatamiApp.factory('TopPostersService', ['$resource', function($resource) { return $resource('/tatami/rest/stats/day'); }]); ================================================ FILE: web/src/main/webapp/app/shared/services/UserService.js ================================================ TatamiApp.factory('UserService', ['$resource', function($resource) { var responseTransform = function(users) { users = angular.fromJson(users); for(var i = 0; i < users.length; i++) { users[i]['avatarURL'] = users[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + users[i].avatar + '/photo.jpg'; } return users; }; return $resource('/tatami/rest/users/:username', null, { 'get': { method: 'GET', params: { username: '@username' }, transformResponse: function(user) { user = angular.fromJson(user); user['avatarURL'] = user.avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + user.avatar + '/photo.jpg'; return user; } }, 'query': { method: 'GET', isArray: true, url: '/tatami/rest/users', transformResponse: responseTransform }, 'getFollowing': { method: 'GET', isArray: true, params: { username: '@username' }, url: '/tatami/rest/users/:username/friends', transformResponse: responseTransform }, 'getFollowers': { method: 'GET', isArray: true, params: { username: '@username' }, url: '/tatami/rest/users/:username/followers', transformResponse: responseTransform }, 'getSuggestions': { method: 'GET', isArray: true, url: '/tatami/rest/users/suggestions', transformResponse: function(suggestions) { suggestions = angular.fromJson(suggestions); for(var i = 0; i < suggestions.length; i++) { suggestions[i]['avatarURL'] = suggestions[i].avatar==='' ? '/assets/img/default_image_profile.png' : '/tatami/avatar/' + suggestions[i].avatar + '/photo.jpg'; suggestions[i]['followingUser'] = false; } return suggestions; } }, 'follow': { method: 'PATCH', params: { username: '@username' } }, 'searchUsers': { method: 'GET', isArray: true, url: '/tatami/rest/users/:term', transformResponse: responseTransform }, 'deactivate': { method: 'PATCH', params: { username: '@username' } } }); }]); ================================================ FILE: web/src/main/webapp/app/shared/services/UserSession.js ================================================ TatamiApp.factory('UserSession', ['$q', '$window', 'ProfileService', 'localStorageService', function($q, $window, ProfileService, localStorageService) { var user; var authenticated = false; return { isAuthenticated: function() { return localStorageService.get('token') === "true"; }, isUserResolved: function() { return angular.isDefined(user); }, setLoginState: function(loggedIn) { localStorageService.set('token', loggedIn); }, clearSession: function() { localStorageService.clearAll(); }, getUser: function() { return user; }, authenticate: function(force) { var deferred = $q.defer(); if(force) { user = undefined; } if(this.isUserResolved() && user.action !== null) { deferred.resolve(user); return deferred.promise; } ProfileService.get(function(data) { // Success user = data; authenticated = true; deferred.resolve(user); }, function() { // Error user = null; authenticated = false; deferred.resolve(user); }); return deferred.promise; } } }]); ================================================ FILE: web/src/main/webapp/app/shared/sidebars/home/HomeSidebarController.js ================================================ HomeSidebarModule.controller('HomeSidebarController', ['$scope', 'UserService', 'TagService', 'profile', 'groups', 'suggestions', 'tags', function($scope, UserService, TagService, profile, groups, suggestions, tags) { $scope.profile = profile; $scope.groups = groups; $scope.suggestions = suggestions; $scope.tags = tags; $scope.followUser = function(suggestion, index) { UserService.follow({ username: suggestion.username }, { friend: !suggestion.followingUser, friendShip: true }, function(response) { $scope.suggestions[index].followingUser = response.friend; }); }; $scope.followTag = function(tag, index) { TagService.follow({ tag: tag.name }, { name: tag.name, followed: !tag.followed, trendingUp: tag.trendingUp }, function(response) { $scope.tags[index].followed = response.followed; $scope.$state.reload(); }); }; } ]); ================================================ FILE: web/src/main/webapp/app/shared/sidebars/home/HomeSidebarModule.js ================================================ var HomeSidebarModule = angular.module('HomeSidebarModule', []); ================================================ FILE: web/src/main/webapp/app/shared/sidebars/home/HomeSidebarView.html ================================================ ================================================ FILE: web/src/main/webapp/app/shared/sidebars/profile/ProfileSidebarController.js ================================================ ProfileSidebarModule.controller('ProfileSidebarController', ['$scope', 'TagService', 'user', 'tags', function($scope, TagService, user, tags) { $scope.user = user; $scope.tags = tags; $scope.followTag = function(tag, index) { TagService.follow({ tag: tag.name }, { name: tag.name, followed: !tag.followed, trendingUp: tag.trendingUp }, function(response) { $scope.tags[index].followed = response.followed; }); } } ]); ================================================ FILE: web/src/main/webapp/app/shared/sidebars/profile/ProfileSidebarModule.js ================================================ var ProfileSidebarModule = angular.module('ProfileSidebarModule', []); ================================================ FILE: web/src/main/webapp/app/shared/sidebars/profile/ProfileSidebarView.html ================================================

    : {{ user.firstName }}

    : {{ user.lastName }}

    : {{ user.jobTitle }}

    : {{ user.login }}

    : {{ user.phoneNumber }}

    ================================================ FILE: web/src/main/webapp/app/shared/topMenu/SearchView.html ================================================
    {{ match.model.name }}
    @{{match.model.username}}
    #{{match.model.name}}
    ================================================ FILE: web/src/main/webapp/app/shared/topMenu/TopMenuController.js ================================================ TopMenuModule.controller('TopMenuController', [ '$scope', '$window', '$http', '$translate', 'amMoment', 'UserSession', 'SearchService', function($scope, $window, $http, $translate, amMoment, UserSession, SearchService) { $scope.current = {}; $scope.current.searchString = ''; $scope.$on('start-tour', function() { $scope.tour.restart(true); }); $scope.changeLanguage = function(key) { $translate.use(key); amMoment.changeLocale(key); }; $scope.openPostModal = function() { $scope.$state.go($scope.$state.current.name + '.post'); }; $scope.logout = function() { $http.get('/tatami/logout') .success(function() { UserSession.clearSession(); $scope.$state.go('tatami.login.main'); $scope.searchString = ''; }); }; // Method to redirect to blog based on language. $scope.goToBlog = function() { var lang = $translate.use(); // If it fails to detect the one being used, it will look for the proposed one. // Useful in asynchronous scenarios. if (lang !== 'fr' && lang !== 'en') { lang = $translate.proposedLanguage(); } switch(lang) { case 'fr': window.open("http://blog.ippon.fr/"); break; default: window.open('http://www.ipponusa.com/blog/'); break; } }; $scope.getResults = function(searchString) { return SearchService.get({ term: 'all', q: searchString }).$promise.then(function(result) { if(angular.isDefined(result.groups[0])) { result.groups[0].firstGroup = true; } if(angular.isDefined(result.tags[0])) { result.tags[0].firstTag = true; } if(angular.isDefined(result.users[0])) { result.users[0].firstUser = true; } return result.groups.concat(result.users.concat(result.tags)); }) }; $scope.changeInput = function(param) { $scope.searchString = param; }; $scope.searchStatuses = function(e) { // If the enter key is pressed if(e.keyCode === 13) { $scope.$state.go('tatami.home.search', { searchTerm: $scope.searchString }); } }; $scope.goToPage = function($item) { if($item.groupId) { $scope.$state.go('tatami.home.home.group.statuses', { groupId: $item.groupId }); } else if($item.login) { $scope.$state.go('tatami.home.profile.statuses', { username: $item.username }); } else if(!$item.groupId) { $scope.$state.go('tatami.home.home.tag', { tag: $item.name }) } }; }]); ================================================ FILE: web/src/main/webapp/app/shared/topMenu/TopMenuModule.js ================================================ var TopMenuModule = angular.module('TopMenuModule', ['PostModule']); ================================================ FILE: web/src/main/webapp/app/shared/topMenu/TopMenuView.html ================================================ ================================================ FILE: web/src/main/webapp/app/shared/topMenu/post/DropdownTagTemplate.html ================================================ ================================================ FILE: web/src/main/webapp/app/shared/topMenu/post/DropdownUserTemplate.html ================================================ ================================================ FILE: web/src/main/webapp/app/shared/topMenu/post/PostController.js ================================================ /** * The purpose of this controller is to collect and maintain data in the status * creation window. */ PostModule.controller('PostController', [ '$scope', '$modalInstance', '$translate', '$upload', 'StatusService', 'GeolocalisationService', 'groups', 'curStatus', 'SearchService', '$stateParams', function($scope, $modalInstance, $translate, $upload, StatusService, GeolocalisationService, groups, curStatus, SearchService, $stateParams) { $scope.isOneDayOrMore = function(date) { return moment().diff(moment(date), 'days', true) >= 1; }; $scope.getLanguageKey = function() { return $translate.use(); }; $scope.determineTitle = function() { if(angular.isDefined(curStatus)) { return 'tatami.home.post.replyTo' } else { return 'tatami.home.post.update' } }; $scope.notArchived = function(groups) { var filteredGroups = []; for(var group in groups) { if(!group.archivedGroup) { filteredGroups.push(group); } } return filteredGroups; }; $scope.current = { // This is the current instance of the status window preview: false, // Determines if the status is being previewed by the user geoLoc: false, // Determine if the geolocalization checkbox is checked groups: groups, // The groups the user belongs to reply: false, // Determine if this status is a reply to another user uploadDone: true,// If the file upload is done, we should not show the progess bar uploadProgress: 0,// The progress of the file currently being uploaded upload: [], contentEmpty: true, files: [], attachments: [] }; $scope.status = { // This is the current user status information content: "", // The content contained in this status groupId: "", // The groupId that this status is being broadcast to replyTo: "", // The user we are replying to attachmentIds: [], // An array of all the attachments contained in the status geoLocalization: "", // The geographical location of the user when posting the status statusPrivate: false // Determines whether the status is private }; $scope.charCount = 750; $scope.uploadStatus = { isUploading: false, progress: 0 }; /** * Watches the current.files ng-model and handles uploads */ $scope.$watch('current.files', function() { if($scope.current.files !== null) { for(var i = 0; i < $scope.current.files.length; ++i) { var file = $scope.current.files[i]; $scope.uploadStatus.isUploading = true; $scope.upload = $upload.upload({ url: '/tatami/rest/fileupload', file: file, fileFormDataName: 'uploadFile' }).progress(function(evt) { $scope.uploadStatus.progress = parseInt(100.0 * evt.loaded / evt.total); }).success(function(data) { $scope.current.attachments.push(data[0]); $scope.uploadStatus.isUploading = false; $scope.uploadStatus.progress = 0; $scope.status.attachmentIds.push(data[0].attachmentId); }).error(function() { $scope.uploadStatus.isUploading = false; $scope.uploadStatus.progress = 0; }) } } }); $scope.fileSize = function(file) { if(file.size / 1000 < 1000) { return parseInt(file.size / 1000) + "K"; } else{ return parseInt(file.size / 1000000) + "M"; } }; /** * In order to set reply to a status, we must be able to set current status * after an asynchronous get request. */ $modalInstance.setCurrentStatus = function(status) { var defined = angular.isDefined(status); $scope.current.reply = defined; $scope.currentStatus = defined ? status : {}; if(defined) { $scope.status.content = '@' + $scope.currentStatus.username + ' '; $scope.status.replyTo = status.statusId; } else { $scope.currentStatus.avatarURL = '/assets/img/default_image_profile.png'; } }; $modalInstance.setCurrentStatus(curStatus); $scope.closeModal = function() { $modalInstance.dismiss(); $scope.reset(); $scope.$state.go('^'); }; //Handles closing the modal via escape and clicking outside the modal $modalInstance.result.finally(function() { $scope.$state.go('^'); }); /** * * @param param String argument representing the most up to date status content * * Simple function to change the current status content */ $scope.statusChange = function(param) { $scope.status.content = param; }; $scope.fetchUsers = function(term) { return SearchService.query({ 'term': 'users', q: term }, function(result) { $scope.users = result; }) }; $scope.selectUser = function(item) { return '@' + item.username; }; $scope.fetchTags = function(term) { if(term.length > 0) { return SearchService.query({ 'term': 'tags', q: term }, function(result) { $scope.tags = result; }); } }; $scope.selectTag = function(item) { return '#' + item.name; }; /** * Resets any previously set status data */ $scope.reset = function() { $scope.current.preview = false; $scope.current.geoLoc = false; $scope.current.uploadDone = true; $scope.current.uploadProgress = 0; $scope.status.content = ""; $scope.status.groupId = ""; $scope.status.attachmentIds = []; $scope.status.geoLocalization = ""; $scope.status.replyTo = ""; $scope.status.statusPrivate = false; }; /** * Create a new status based on the current data in the controller. * Uses the StatusService for this purpose, and we do nothing if no * content has been provided by the user. */ var isAlreadyPosting=false; $scope.newStatus = function() { if($scope.status.content.trim().length !== 0 && !isAlreadyPosting) { isAlreadyPosting=true; $scope.status.content = $scope.status.content.trim(); StatusService.save($scope.status, function() { $modalInstance.close(); $modalInstance.result.then(function() { $scope.$state.transitionTo($scope.$state.current.name.split('.post')[0], $stateParams, { reload: true }); }); $scope.reset(); }) } }; /** Geolocalization based functions **/ /** * Determine whether the user means to use location data on the current status */ $scope.updateLocation = function() { if($scope.current.geoLoc) { GeolocalisationService.getGeolocalisation($scope.getLocationString); } else { $scope.status.geoLocalization = ""; } }; /** * Callback function used in getGeolocalisation. This function sets the status geolocation, * and brings up a map. */ $scope.getLocationString = function(position) { $scope.status.geoLocalization = position.coords.latitude + ", " + position.coords.longitude; $scope.initMap(); }; /** * Create a map displaying the users current location in the status */ $scope.initMap = function() { if ($scope.current.geoLoc) { var geoLocalization = $scope.status.geoLocalization; var latitude = geoLocalization.split(',')[0].trim(); var longitude = geoLocalization.split(',')[1].trim(); var map = new OpenLayers.Map("simpleMap"); var fromProjection = new OpenLayers.Projection("EPSG:4326");// Transform from WGS 1984 var toProjection = new OpenLayers.Projection("EPSG:900913");// to Spherical Mercator Projection var lonLat = new OpenLayers.LonLat(parseFloat(longitude), parseFloat(latitude)).transform(fromProjection, toProjection); var mapnik = new OpenLayers.Layer.OSM(); var position = lonLat; var zoom = 12; map.addLayer(mapnik); var markers = new OpenLayers.Layer.Markers("Markers"); map.addLayer(markers); markers.addMarker(new OpenLayers.Marker(lonLat)); map.setCenter(position, zoom); } } } ]); ================================================ FILE: web/src/main/webapp/app/shared/topMenu/post/PostModule.js ================================================ var PostModule = angular.module('PostModule', ['angularFileUpload', 'ui.router']); PostModule.config(['$stateProvider', function ($stateProvider) { var onEnterArray = ['$stateParams', '$modal', function ($stateParams, $modal) { $modal.open({ templateUrl: '/app/shared/topMenu/post/PostView.min.html', controller: 'PostController', backdrop: 'static', keyboard: true, resolve: { curStatus: ['StatusService', function (StatusService) { if ($stateParams.statusId !== null) { return StatusService.get({statusId: $stateParams.statusId}).$promise; } }], groups: ['GroupService', function (GroupService) { return GroupService.query().$promise; }] } }); }]; var onEnterArrayStatusView = ['$stateParams', '$modal', function ($stateParams, $modal) { $modal.open({ templateUrl: '/app/shared/topMenu/post/PostView.min.html', controller: 'PostController', backdrop: 'static', keyboard: true, resolve: { curStatus: ['StatusService', function (StatusService) { if ($stateParams.statusIdReply !== null) { return StatusService.get({statusId: $stateParams.statusIdReply}).$promise; } }], groups: ['GroupService', function (GroupService) { return GroupService.query().$promise; }] } }); }]; $stateProvider .state('tatami.home.status.post', { url: '', params: { 'statusIdReply': null }, onEnter: onEnterArrayStatusView }) .state('tatami.home.search.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.home.timeline.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.home.mentions.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.home.favorites.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.home.company.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.home.tag.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.home.group.statuses.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.home.group.members.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.profile.statuses.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.profile.following.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }) .state('tatami.home.profile.followers.post', { url: '', params: { 'statusId': null }, onEnter: onEnterArray }); } ]); ================================================ FILE: web/src/main/webapp/app/shared/topMenu/post/PostView.html ================================================ ================================================ FILE: web/src/main/webapp/assets/bower_components/angular/.bower.json ================================================ { "name": "angular", "version": "1.3.15", "main": "./angular.js", "ignore": [], "dependencies": {}, "homepage": "https://github.com/angular/bower-angular", "_release": "1.3.15", "_resolution": { "type": "version", "tag": "v1.3.15", "commit": "ba7abcfa409ba852146e6ba206693cf7bac3e359" }, "_source": "git://github.com/angular/bower-angular.git", "_target": "1.3.15", "_originalSource": "angular" } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular/README.md ================================================ # packaged angular This repo is for distribution on `npm` and `bower`. The source for this module is in the [main AngularJS repo](https://github.com/angular/angular.js). Please file issues and pull requests against that repo. ## Install You can install this package either with `npm` or with `bower`. ### npm ```shell npm install angular ``` Then add a ` ``` Or `require('angular')` from your code. ### bower ```shell bower install angular ``` Then add a ` ``` ## Documentation Documentation is available on the [AngularJS docs site](http://docs.angularjs.org/). ## License The MIT License Copyright (c) 2010-2015 Google, Inc. http://angularjs.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: web/src/main/webapp/assets/bower_components/angular/angular-csp.css ================================================ /* Include this file in your html if you are using the CSP mode. */ @charset "UTF-8"; [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak, .ng-hide:not(.ng-hide-animate) { display: none !important; } ng\:form { display: block; } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular/bower.json ================================================ { "name": "angular", "version": "1.3.15", "main": "./angular.js", "ignore": [], "dependencies": { } } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular/index.js ================================================ require('./angular'); module.exports = angular; ================================================ FILE: web/src/main/webapp/assets/bower_components/angular/package.json ================================================ { "name": "angular", "version": "1.3.15", "description": "HTML enhanced for web apps", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "https://github.com/angular/angular.js.git" }, "keywords": [ "angular", "framework", "browser", "client-side" ], "author": "Angular Core Team ", "license": "MIT", "bugs": { "url": "https://github.com/angular/angular.js/issues" }, "homepage": "http://angularjs.org" } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-animate/.bower.json ================================================ { "name": "angular-animate", "version": "1.3.15", "main": "./angular-animate.js", "ignore": [], "dependencies": { "angular": "1.3.15" }, "homepage": "https://github.com/angular/bower-angular-animate", "_release": "1.3.15", "_resolution": { "type": "version", "tag": "v1.3.15", "commit": "30fb369974560dbeb8a5311861c124e094944dab" }, "_source": "git://github.com/angular/bower-angular-animate.git", "_target": "~1.3.15", "_originalSource": "angular-animate", "_direct": true } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-animate/README.md ================================================ # packaged angular-animate This repo is for distribution on `npm` and `bower`. The source for this module is in the [main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngAnimate). Please file issues and pull requests against that repo. ## Install You can install this package either with `npm` or with `bower`. ### npm ```shell npm install angular-animate ``` Then add `ngAnimate` as a dependency for your app: ```javascript angular.module('myApp', [require('angular-animate')]); ``` ### bower ```shell bower install angular-animate ``` Then add a ` ``` Then add `ngAnimate` as a dependency for your app: ```javascript angular.module('myApp', ['ngAnimate']); ``` ## Documentation Documentation is available on the [AngularJS docs site](http://docs.angularjs.org/api/ngAnimate). ## License The MIT License Copyright (c) 2010-2015 Google, Inc. http://angularjs.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-animate/bower.json ================================================ { "name": "angular-animate", "version": "1.3.15", "main": "./angular-animate.js", "ignore": [], "dependencies": { "angular": "1.3.15" } } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-animate/index.js ================================================ require('./angular-animate'); module.exports = 'ngAnimate'; ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-animate/package.json ================================================ { "name": "angular-animate", "version": "1.3.15", "description": "AngularJS module for animations", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "https://github.com/angular/angular.js.git" }, "keywords": [ "angular", "framework", "browser", "animation", "client-side" ], "author": "Angular Core Team ", "license": "MIT", "bugs": { "url": "https://github.com/angular/angular.js/issues" }, "homepage": "http://angularjs.org" } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-bootstrap/.bower.json ================================================ { "author": { "name": "https://github.com/angular-ui/bootstrap/graphs/contributors" }, "name": "angular-bootstrap", "keywords": [ "angular", "angular-ui", "bootstrap" ], "license": "MIT", "ignore": [], "description": "Native AngularJS (Angular) directives for Bootstrap.", "version": "0.12.1", "main": [ "./ui-bootstrap-tpls.js" ], "dependencies": { "angular": ">=1 <1.3.0" }, "homepage": "https://github.com/angular-ui/bootstrap-bower", "_release": "0.12.1", "_resolution": { "type": "version", "tag": "0.12.1", "commit": "ab14fbaaf3d592f8e76018f0666c5af6f68ebaa3" }, "_source": "git://github.com/angular-ui/bootstrap-bower.git", "_target": "~0.12.1", "_originalSource": "angular-bootstrap", "_direct": true } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-bootstrap/bower.json ================================================ { "author": { "name": "https://github.com/angular-ui/bootstrap/graphs/contributors" }, "name": "angular-bootstrap", "keywords": [ "angular", "angular-ui", "bootstrap" ], "license": "MIT", "ignore": [], "description": "Native AngularJS (Angular) directives for Bootstrap.", "version": "0.12.1", "main": ["./ui-bootstrap-tpls.js"], "dependencies": { "angular": ">=1 <1.3.0" } } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-bootstrap-tour/.bower.json ================================================ { "name": "angular-bootstrap-tour", "version": "0.3.2", "main": "dist/angular-bootstrap-tour.js", "ignore": [ ".editorconfig", ".gitattributes", ".gitignore", ".jshintrc", ".npmignore", ".travis.yml", ".umd", "gulpfile.js", "npm-shrinkwrap.json", "package.json" ], "dependencies": { "angular": "~1.2.23", "bootstrap-tour": "~0.9.3", "jquery": "1.11.1", "bootstrap": "3.0.3", "angular-route": "1.2.16" }, "devDependencies": { "html5shiv": "~3.7.2", "respond": "~1.4.2" }, "resolutions": { "angular": "1.2.16" }, "homepage": "https://github.com/benmarch/angular-bootstrap-tour", "_release": "0.3.2", "_resolution": { "type": "version", "tag": "0.3.2", "commit": "ff18966e4ede35f952517fee34f7dd73afa7a467" }, "_source": "git://github.com/benmarch/angular-bootstrap-tour.git", "_target": "~0.3.2", "_originalSource": "angular-bootstrap-tour", "_direct": true } ================================================ FILE: web/src/main/webapp/assets/bower_components/angular-bootstrap-tour/README.md ================================================ # angular-bootstrap-tour [![Bower Version][bower-image]][bower-url] ## About This is a simple Angular wrapper around [Bootstrap Tour](http://www.bootstraptour.com). Simply add the "tour" directive anywhere, and add the "tour-step" directive to any element within "tour" that needs a tip. All [options](http://bootstraptour.com/api/) are available by adding the corresponding attributes to the directive element. There is also a "skip" option that if evaluates to true, will skip over the step. This repository was scaffolded with [generator-microjs](https://github.com/daniellmb/generator-microjs). ## Getting Started Get the package: bower install angular-bootstrap-tour Add the script tags: And the Bootstrap Tour CSS (or create your own): Then add the module to your app: angular.module('myApp', ['bm.bsTour']); ## Configuration The TourConfigProvider allows you to set a couple options: - `prefixOptions` {boolean, default: false} if set to true will require directive options to be prefixed to avoid conflicts - `prefix` {string, default: 'bsTour'} the prefix to use if `prefixOptions` is set to `true` Use `TourConfigProvider.set(