From e52f81bf2187a81c9ca3498680af4c3aea03d607 Mon Sep 17 00:00:00 2001 From: Max Klyga Date: Mon, 10 Dec 2018 19:20:20 +0100 Subject: [PATCH 1/8] New client --- .gitignore | 31 +- .java-version | 1 + .travis/publish.sh | 26 - .travis/settings.xml | 9 - LICENSE | 48 - README.md | 181 ---- build.gradle | 131 +++ data/test.jpg | Bin 0 -> 1362 bytes data/test.txt | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56177 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 ++++ gradlew.bat | 84 ++ pom.xml | 331 ------- settings.gradle | 10 + src/main/java/example/Example.java | 326 +++++++ .../io/getstream/client/AggregatedFeed.java | 184 ++++ .../io/getstream/client/AnalyticsClient.java | 56 ++ .../java/io/getstream/client/BatchClient.java | 80 ++ src/main/java/io/getstream/client/Client.java | 296 ++++++ .../getstream/client/CollectionsClient.java | 139 +++ src/main/java/io/getstream/client/Feed.java | 278 ++++++ .../getstream/client/FileStorageClient.java | 37 + .../java/io/getstream/client/FlatFeed.java | 196 ++++ .../getstream/client/ImageStorageClient.java | 49 + .../io/getstream/client/NotificationFeed.java | 216 +++++ .../client/PersonalizationClient.java | 73 ++ .../io/getstream/client/ReactionsClient.java | 98 ++ src/main/java/io/getstream/client/User.java | 98 ++ .../getstream/cloud/CloudAggregatedFeed.java | 184 ++++ .../java/io/getstream/cloud/CloudClient.java | 259 +++++ .../cloud/CloudCollectionsClient.java | 81 ++ .../java/io/getstream/cloud/CloudFeed.java | 246 +++++ .../cloud/CloudFileStorageClient.java | 31 + .../io/getstream/cloud/CloudFlatFeed.java | 196 ++++ .../cloud/CloudImageStorageClient.java | 41 + .../cloud/CloudNotificationFeed.java | 216 +++++ .../getstream/cloud/CloudReactionsClient.java | 109 +++ .../java/io/getstream/cloud/CloudUser.java | 98 ++ .../java/io/getstream/core/KeepHistory.java | 16 + .../java/io/getstream/core/LookupKind.java | 16 + src/main/java/io/getstream/core/Region.java | 20 + src/main/java/io/getstream/core/Stream.java | 369 ++++++++ .../io/getstream/core/StreamAnalytics.java | 93 ++ .../java/io/getstream/core/StreamBatch.java | 183 ++++ .../io/getstream/core/StreamCollections.java | 220 +++++ .../java/io/getstream/core/StreamFiles.java | 90 ++ .../java/io/getstream/core/StreamImages.java | 110 +++ .../getstream/core/StreamPersonalization.java | 108 +++ .../io/getstream/core/StreamReactions.java | 168 ++++ .../core/exceptions/StreamAPIException.java | 60 ++ .../core/exceptions/StreamException.java | 23 + .../io/getstream/core/http/HTTPClient.java | 12 + .../core/http/OKHTTPClientAdapter.java | 114 +++ .../java/io/getstream/core/http/Request.java | 157 ++++ .../io/getstream/core/http/RequestBody.java | 93 ++ .../java/io/getstream/core/http/Response.java | 49 + .../java/io/getstream/core/http/Token.java | 35 + .../io/getstream/core/models/Activity.java | 265 ++++++ .../getstream/core/models/CollectionData.java | 105 +++ .../io/getstream/core/models/Content.java | 86 ++ .../java/io/getstream/core/models/Data.java | 86 ++ .../io/getstream/core/models/Engagement.java | 210 +++++ .../core/models/EnrichedActivity.java | 300 ++++++ .../io/getstream/core/models/Feature.java | 48 + .../java/io/getstream/core/models/FeedID.java | 62 ++ .../getstream/core/models/FollowRelation.java | 55 ++ .../core/models/ForeignIDTimePair.java | 46 + .../java/io/getstream/core/models/Group.java | 93 ++ .../io/getstream/core/models/Impression.java | 180 ++++ .../core/models/NotificationGroup.java | 76 ++ .../java/io/getstream/core/models/OGData.java | 271 ++++++ .../io/getstream/core/models/ProfileData.java | 86 ++ .../io/getstream/core/models/Reaction.java | 160 ++++ .../io/getstream/core/models/UserData.java | 45 + .../serialization/DataDeserializer.java | 37 + .../core/options/ActivityMarker.java | 63 ++ .../java/io/getstream/core/options/Crop.java | 70 ++ .../core/options/CustomQueryParameter.java | 33 + .../io/getstream/core/options/Filter.java | 65 ++ .../getstream/core/options/KeepHistory.java | 20 + .../io/getstream/core/options/Pagination.java | 57 ++ .../io/getstream/core/options/Ranking.java | 25 + .../getstream/core/options/RequestOption.java | 7 + .../io/getstream/core/options/Resize.java | 63 ++ .../java/io/getstream/core/utils/Auth.java | 137 +++ .../getstream/core/utils/DefaultOptions.java | 14 + .../io/getstream/core/utils/Enrichment.java | 11 + .../java/io/getstream/core/utils/Info.java | 30 + .../java/io/getstream/core/utils/Request.java | 48 + .../java/io/getstream/core/utils/Routes.java | 116 +++ .../getstream/core/utils/Serialization.java | 131 +++ src/main/resources/stream-java2.info | 3 + .../getstream/client/AggregatedFeedTest.java | 91 ++ .../getstream/client/AnalyticsClientTest.java | 88 ++ .../io/getstream/client/BatchClientTest.java | 102 ++ .../java/io/getstream/client/ClientTest.java | 133 +++ .../client/CollectionsClientTest.java | 246 +++++ .../java/io/getstream/client/FeedTest.java | 199 ++++ .../client/FileStorageClientTest.java | 52 + .../io/getstream/client/FlatFeedTest.java | 90 ++ .../client/ImageStorageClientTest.java | 96 ++ .../client/NotificationFeedTest.java | 43 + src/test/java/io/getstream/client/OGTest.java | 24 + .../client/PersonalizationClientTest.java | 63 ++ .../getstream/client/ReactionsClientTest.java | 77 ++ .../java/io/getstream/client/UserTest.java | 129 +++ .../client/entities/FootballMatch.java | 43 + .../io/getstream/client/entities/Match.java | 37 + .../client/entities/VolleyballMatch.java | 43 + .../core/http/OKHTTPClientAdapterTest.java | 68 ++ .../core/utils/ActivityGenerator.java | 83 ++ .../utils/SerializationPropertiesTest.java | 26 + .../core/utils/SerializationTest.java | 229 +++++ stream-core/pom.xml | 73 -- .../io/getstream/client/StreamClient.java | 48 - .../AuthenticationHandlerConfiguration.java | 34 - .../client/config/ClientConfiguration.java | 174 ---- .../getstream/client/config/StreamRegion.java | 37 - .../AuthenticationFailedException.java | 27 - .../exception/InternalServerException.java | 26 - .../exception/InvalidFeedNameException.java | 27 - .../InvalidOrMissingInputException.java | 27 - .../exception/RateLimitExceededException.java | 27 - .../exception/ResourceNotFoundException.java | 27 - .../exception/StreamClientException.java | 75 -- .../client/exception/UriBuilderException.java | 26 - .../ActivitySignedRecipientDeserializer.java | 48 - .../model/activities/AggregatedActivity.java | 4 - .../client/model/activities/BaseActivity.java | 150 --- .../activities/NotificationActivity.java | 29 - .../activities/PersonalizedActivity.java | 33 - .../model/activities/SimpleActivity.java | 8 - .../activities/UpdateTargetResponse.java | 80 -- .../model/activities/WrappedActivity.java | 87 -- .../getstream/client/model/beans/AddMany.java | 38 - .../client/model/beans/FeedFollow.java | 59 -- .../client/model/beans/FollowMany.java | 82 -- .../client/model/beans/FollowRequest.java | 36 - .../client/model/beans/MarkedActivity.java | 61 -- .../client/model/beans/MetaResponse.java | 40 - .../model/beans/StreamActivitiesResponse.java | 22 - .../model/beans/StreamErrorResponse.java | 51 - .../client/model/beans/StreamResponse.java | 56 -- .../getstream/client/model/beans/Targets.java | 126 --- .../client/model/beans/UnfollowMany.java | 111 --- .../client/model/beans/UpdateTo.java | 49 - .../client/model/feeds/BaseFeed.java | 161 ---- .../client/model/feeds/BaseFeedFactory.java | 51 - .../io/getstream/client/model/feeds/Feed.java | 210 ----- .../client/model/feeds/FeedFactory.java | 39 - .../client/model/feeds/PersonalizedFeed.java | 64 -- .../model/feeds/PersonalizedFeedImpl.java | 50 - .../client/model/filters/FeedFilter.java | 135 --- .../repo/StreamPersonalizedRepository.java | 55 -- .../client/repo/StreamRepoFactory.java | 22 - .../client/repo/StreamRepository.java | 263 ------ .../service/AbstractActivityService.java | 97 -- .../service/AggregatedActivityService.java | 36 - .../AggregatedActivityServiceImpl.java | 34 - .../client/service/FlatActivityService.java | 35 - .../service/FlatActivityServiceImpl.java | 33 - .../service/NotificationActivityService.java | 53 -- .../NotificationActivityServiceImpl.java | 40 - .../client/service/UserActivityService.java | 35 - .../service/UserActivityServiceImpl.java | 35 - .../client/util/DateDeserializer.java | 45 - .../getstream/client/util/EndpointUtil.java | 58 -- .../client/util/HttpSignatureHandler.java | 51 - .../io/getstream/client/util/InfoUtil.java | 34 - .../client/util/JwtAuthenticationUtil.java | 81 -- .../util/PersonalizedDateDeserializer.java | 45 - .../client/config/StreamRegionTest.java | 14 - .../client/model/beans/TargetsTest.java | 39 - .../client/model/filters/FeedFilterTest.java | 64 -- .../AggregatedActivityServiceImplTest.java | 59 -- .../service/FlatActivityServiceImplTest.java | 58 -- .../NotificationActivityServiceImplTest.java | 71 -- .../service/UserActivityServiceImplTest.java | 58 -- .../client/util/DateDeserializerTest.java | 47 - .../client/util/EndpointUtilTest.java | 57 -- .../getstream/client/util/InfoUtilTest.java | 16 - .../util/JwtAuthenticationUtilTest.java | 95 -- .../src/test/resources/stream-java.info | 1 - stream-repo-apache/pom.xml | 94 -- .../client/apache/StreamClientImpl.java | 145 --- .../apache/repo/HttpSignatureInterceptor.java | 44 - .../apache/repo/StreamActivityRepository.java | 270 ------ .../StreamPersonalizedRepositoryImpl.java | 133 --- .../apache/repo/StreamRepositoryImpl.java | 253 ----- .../repo/handlers/StreamExceptionHandler.java | 87 -- .../apache/repo/utils/FeedFilterUtils.java | 52 - .../apache/repo/utils/SignatureUtils.java | 76 -- .../apache/repo/utils/StreamRepoUtils.java | 89 -- .../client/apache/repo/utils/UriBuilder.java | 119 --- .../src/main/resources/stream-java.info | 3 - .../client/apache/IntegrationTest.java | 849 ----------------- .../apache/PersonalizedIntegrationTest.java | 151 --- .../client/apache/StreamClientImplTest.java | 50 - .../client/apache/example/follow/Follow.java | 63 -- .../apache/example/helloworld/HelloWorld.java | 65 -- .../apache/example/mixtype/MixedType.java | 135 --- ...tivitySignedRecipientDeserializerTest.java | 37 - .../StreamPersonalizedRepositoryImplTest.java | 159 ---- .../apache/repo/StreamRepositoryImplTest.java | 86 -- .../handlers/StreamExceptionHandlerTest.java | 92 -- .../repo/utils/StreamRepoUtilsTest.java | 63 -- .../client/apache/utils/UriBuilderTest.java | 34 - stream-repo-okhttp/pom.xml | 95 -- .../client/okhttp/StreamClientImpl.java | 146 --- .../okhttp/repo/HttpSignatureinterceptor.java | 44 - .../okhttp/repo/StreamActivityRepository.java | 293 ------ .../StreamPersonalizedRepositoryImpl.java | 133 --- .../okhttp/repo/StreamRepositoryImpl.java | 256 ----- .../repo/handlers/StreamExceptionHandler.java | 81 -- .../client/okhttp/repo/utils/Base64.java | 742 --------------- .../okhttp/repo/utils/FeedFilterUtils.java | 52 - .../okhttp/repo/utils/SignatureUtils.java | 74 -- .../okhttp/repo/utils/StreamRepoUtils.java | 74 -- .../client/okhttp/repo/utils/UriBuilder.java | 149 --- .../src/main/resources/stream-java.info | 3 - .../client/okhttp/IntegrationTest.java | 888 ------------------ .../okhttp/PersonalizedIntegrationTest.java | 139 --- .../client/okhttp/StreamClientImplTest.java | 38 - .../StreamPersonalizedRepositoryImplTest.java | 160 ---- .../handlers/StreamExceptionHandlerTest.java | 120 --- .../okhttp/repo/utils/InfoUtilTest.java | 17 - .../repo/utils/StreamRepoUtilsTest.java | 55 -- .../okhttp/repo/utils/UriBuilderTest.java | 33 - 229 files changed, 10999 insertions(+), 11500 deletions(-) create mode 100644 .java-version delete mode 100755 .travis/publish.sh delete mode 100644 .travis/settings.xml delete mode 100644 LICENSE delete mode 100644 README.md create mode 100644 build.gradle create mode 100644 data/test.jpg create mode 100644 data/test.txt create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat delete mode 100644 pom.xml create mode 100644 settings.gradle create mode 100644 src/main/java/example/Example.java create mode 100644 src/main/java/io/getstream/client/AggregatedFeed.java create mode 100644 src/main/java/io/getstream/client/AnalyticsClient.java create mode 100644 src/main/java/io/getstream/client/BatchClient.java create mode 100644 src/main/java/io/getstream/client/Client.java create mode 100644 src/main/java/io/getstream/client/CollectionsClient.java create mode 100644 src/main/java/io/getstream/client/Feed.java create mode 100644 src/main/java/io/getstream/client/FileStorageClient.java create mode 100644 src/main/java/io/getstream/client/FlatFeed.java create mode 100644 src/main/java/io/getstream/client/ImageStorageClient.java create mode 100644 src/main/java/io/getstream/client/NotificationFeed.java create mode 100644 src/main/java/io/getstream/client/PersonalizationClient.java create mode 100644 src/main/java/io/getstream/client/ReactionsClient.java create mode 100644 src/main/java/io/getstream/client/User.java create mode 100644 src/main/java/io/getstream/cloud/CloudAggregatedFeed.java create mode 100644 src/main/java/io/getstream/cloud/CloudClient.java create mode 100644 src/main/java/io/getstream/cloud/CloudCollectionsClient.java create mode 100644 src/main/java/io/getstream/cloud/CloudFeed.java create mode 100644 src/main/java/io/getstream/cloud/CloudFileStorageClient.java create mode 100644 src/main/java/io/getstream/cloud/CloudFlatFeed.java create mode 100644 src/main/java/io/getstream/cloud/CloudImageStorageClient.java create mode 100644 src/main/java/io/getstream/cloud/CloudNotificationFeed.java create mode 100644 src/main/java/io/getstream/cloud/CloudReactionsClient.java create mode 100644 src/main/java/io/getstream/cloud/CloudUser.java create mode 100644 src/main/java/io/getstream/core/KeepHistory.java create mode 100644 src/main/java/io/getstream/core/LookupKind.java create mode 100644 src/main/java/io/getstream/core/Region.java create mode 100644 src/main/java/io/getstream/core/Stream.java create mode 100644 src/main/java/io/getstream/core/StreamAnalytics.java create mode 100644 src/main/java/io/getstream/core/StreamBatch.java create mode 100644 src/main/java/io/getstream/core/StreamCollections.java create mode 100644 src/main/java/io/getstream/core/StreamFiles.java create mode 100644 src/main/java/io/getstream/core/StreamImages.java create mode 100644 src/main/java/io/getstream/core/StreamPersonalization.java create mode 100644 src/main/java/io/getstream/core/StreamReactions.java create mode 100644 src/main/java/io/getstream/core/exceptions/StreamAPIException.java create mode 100644 src/main/java/io/getstream/core/exceptions/StreamException.java create mode 100644 src/main/java/io/getstream/core/http/HTTPClient.java create mode 100644 src/main/java/io/getstream/core/http/OKHTTPClientAdapter.java create mode 100644 src/main/java/io/getstream/core/http/Request.java create mode 100644 src/main/java/io/getstream/core/http/RequestBody.java create mode 100644 src/main/java/io/getstream/core/http/Response.java create mode 100644 src/main/java/io/getstream/core/http/Token.java create mode 100644 src/main/java/io/getstream/core/models/Activity.java create mode 100644 src/main/java/io/getstream/core/models/CollectionData.java create mode 100644 src/main/java/io/getstream/core/models/Content.java create mode 100644 src/main/java/io/getstream/core/models/Data.java create mode 100644 src/main/java/io/getstream/core/models/Engagement.java create mode 100644 src/main/java/io/getstream/core/models/EnrichedActivity.java create mode 100644 src/main/java/io/getstream/core/models/Feature.java create mode 100644 src/main/java/io/getstream/core/models/FeedID.java create mode 100644 src/main/java/io/getstream/core/models/FollowRelation.java create mode 100644 src/main/java/io/getstream/core/models/ForeignIDTimePair.java create mode 100644 src/main/java/io/getstream/core/models/Group.java create mode 100644 src/main/java/io/getstream/core/models/Impression.java create mode 100644 src/main/java/io/getstream/core/models/NotificationGroup.java create mode 100644 src/main/java/io/getstream/core/models/OGData.java create mode 100644 src/main/java/io/getstream/core/models/ProfileData.java create mode 100644 src/main/java/io/getstream/core/models/Reaction.java create mode 100644 src/main/java/io/getstream/core/models/UserData.java create mode 100644 src/main/java/io/getstream/core/models/serialization/DataDeserializer.java create mode 100644 src/main/java/io/getstream/core/options/ActivityMarker.java create mode 100644 src/main/java/io/getstream/core/options/Crop.java create mode 100644 src/main/java/io/getstream/core/options/CustomQueryParameter.java create mode 100644 src/main/java/io/getstream/core/options/Filter.java create mode 100644 src/main/java/io/getstream/core/options/KeepHistory.java create mode 100644 src/main/java/io/getstream/core/options/Pagination.java create mode 100644 src/main/java/io/getstream/core/options/Ranking.java create mode 100644 src/main/java/io/getstream/core/options/RequestOption.java create mode 100644 src/main/java/io/getstream/core/options/Resize.java create mode 100644 src/main/java/io/getstream/core/utils/Auth.java create mode 100644 src/main/java/io/getstream/core/utils/DefaultOptions.java create mode 100644 src/main/java/io/getstream/core/utils/Enrichment.java create mode 100644 src/main/java/io/getstream/core/utils/Info.java create mode 100644 src/main/java/io/getstream/core/utils/Request.java create mode 100644 src/main/java/io/getstream/core/utils/Routes.java create mode 100644 src/main/java/io/getstream/core/utils/Serialization.java create mode 100644 src/main/resources/stream-java2.info create mode 100644 src/test/java/io/getstream/client/AggregatedFeedTest.java create mode 100644 src/test/java/io/getstream/client/AnalyticsClientTest.java create mode 100644 src/test/java/io/getstream/client/BatchClientTest.java create mode 100644 src/test/java/io/getstream/client/ClientTest.java create mode 100644 src/test/java/io/getstream/client/CollectionsClientTest.java create mode 100644 src/test/java/io/getstream/client/FeedTest.java create mode 100644 src/test/java/io/getstream/client/FileStorageClientTest.java create mode 100644 src/test/java/io/getstream/client/FlatFeedTest.java create mode 100644 src/test/java/io/getstream/client/ImageStorageClientTest.java create mode 100644 src/test/java/io/getstream/client/NotificationFeedTest.java create mode 100644 src/test/java/io/getstream/client/OGTest.java create mode 100644 src/test/java/io/getstream/client/PersonalizationClientTest.java create mode 100644 src/test/java/io/getstream/client/ReactionsClientTest.java create mode 100644 src/test/java/io/getstream/client/UserTest.java create mode 100644 src/test/java/io/getstream/client/entities/FootballMatch.java create mode 100644 src/test/java/io/getstream/client/entities/Match.java create mode 100644 src/test/java/io/getstream/client/entities/VolleyballMatch.java create mode 100644 src/test/java/io/getstream/core/http/OKHTTPClientAdapterTest.java create mode 100644 src/test/java/io/getstream/core/utils/ActivityGenerator.java create mode 100644 src/test/java/io/getstream/core/utils/SerializationPropertiesTest.java create mode 100644 src/test/java/io/getstream/core/utils/SerializationTest.java delete mode 100644 stream-core/pom.xml delete mode 100644 stream-core/src/main/java/io/getstream/client/StreamClient.java delete mode 100644 stream-core/src/main/java/io/getstream/client/config/AuthenticationHandlerConfiguration.java delete mode 100644 stream-core/src/main/java/io/getstream/client/config/ClientConfiguration.java delete mode 100644 stream-core/src/main/java/io/getstream/client/config/StreamRegion.java delete mode 100644 stream-core/src/main/java/io/getstream/client/exception/AuthenticationFailedException.java delete mode 100644 stream-core/src/main/java/io/getstream/client/exception/InternalServerException.java delete mode 100644 stream-core/src/main/java/io/getstream/client/exception/InvalidFeedNameException.java delete mode 100644 stream-core/src/main/java/io/getstream/client/exception/InvalidOrMissingInputException.java delete mode 100644 stream-core/src/main/java/io/getstream/client/exception/RateLimitExceededException.java delete mode 100644 stream-core/src/main/java/io/getstream/client/exception/ResourceNotFoundException.java delete mode 100644 stream-core/src/main/java/io/getstream/client/exception/StreamClientException.java delete mode 100644 stream-core/src/main/java/io/getstream/client/exception/UriBuilderException.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/activities/ActivitySignedRecipientDeserializer.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/activities/AggregatedActivity.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/activities/BaseActivity.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/activities/NotificationActivity.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/activities/PersonalizedActivity.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/activities/SimpleActivity.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/activities/UpdateTargetResponse.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/activities/WrappedActivity.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/AddMany.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/FeedFollow.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/FollowMany.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/FollowRequest.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/MarkedActivity.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/MetaResponse.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/StreamActivitiesResponse.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/StreamErrorResponse.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/StreamResponse.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/Targets.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/UnfollowMany.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/beans/UpdateTo.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/feeds/BaseFeed.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/feeds/BaseFeedFactory.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/feeds/Feed.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/feeds/FeedFactory.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/feeds/PersonalizedFeed.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/feeds/PersonalizedFeedImpl.java delete mode 100644 stream-core/src/main/java/io/getstream/client/model/filters/FeedFilter.java delete mode 100644 stream-core/src/main/java/io/getstream/client/repo/StreamPersonalizedRepository.java delete mode 100644 stream-core/src/main/java/io/getstream/client/repo/StreamRepoFactory.java delete mode 100644 stream-core/src/main/java/io/getstream/client/repo/StreamRepository.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/AbstractActivityService.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/AggregatedActivityService.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/AggregatedActivityServiceImpl.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/FlatActivityService.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/FlatActivityServiceImpl.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/NotificationActivityService.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/NotificationActivityServiceImpl.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/UserActivityService.java delete mode 100644 stream-core/src/main/java/io/getstream/client/service/UserActivityServiceImpl.java delete mode 100644 stream-core/src/main/java/io/getstream/client/util/DateDeserializer.java delete mode 100644 stream-core/src/main/java/io/getstream/client/util/EndpointUtil.java delete mode 100644 stream-core/src/main/java/io/getstream/client/util/HttpSignatureHandler.java delete mode 100644 stream-core/src/main/java/io/getstream/client/util/InfoUtil.java delete mode 100644 stream-core/src/main/java/io/getstream/client/util/JwtAuthenticationUtil.java delete mode 100644 stream-core/src/main/java/io/getstream/client/util/PersonalizedDateDeserializer.java delete mode 100644 stream-core/src/test/java/io/getstream/client/config/StreamRegionTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/model/beans/TargetsTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/model/filters/FeedFilterTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/service/AggregatedActivityServiceImplTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/service/FlatActivityServiceImplTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/service/NotificationActivityServiceImplTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/service/UserActivityServiceImplTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/util/DateDeserializerTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/util/EndpointUtilTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/util/InfoUtilTest.java delete mode 100644 stream-core/src/test/java/io/getstream/client/util/JwtAuthenticationUtilTest.java delete mode 100644 stream-core/src/test/resources/stream-java.info delete mode 100644 stream-repo-apache/pom.xml delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/StreamClientImpl.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/HttpSignatureInterceptor.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/StreamActivityRepository.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/StreamPersonalizedRepositoryImpl.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/StreamRepositoryImpl.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/handlers/StreamExceptionHandler.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/utils/FeedFilterUtils.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/utils/SignatureUtils.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/utils/StreamRepoUtils.java delete mode 100644 stream-repo-apache/src/main/java/io/getstream/client/apache/repo/utils/UriBuilder.java delete mode 100644 stream-repo-apache/src/main/resources/stream-java.info delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/IntegrationTest.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/PersonalizedIntegrationTest.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/StreamClientImplTest.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/example/follow/Follow.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/example/helloworld/HelloWorld.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/example/mixtype/MixedType.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/model/activities/ActivitySignedRecipientDeserializerTest.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/repo/StreamPersonalizedRepositoryImplTest.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/repo/StreamRepositoryImplTest.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/repo/handlers/StreamExceptionHandlerTest.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/repo/utils/StreamRepoUtilsTest.java delete mode 100644 stream-repo-apache/src/test/java/io/getstream/client/apache/utils/UriBuilderTest.java delete mode 100644 stream-repo-okhttp/pom.xml delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/StreamClientImpl.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/HttpSignatureinterceptor.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/StreamActivityRepository.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/StreamPersonalizedRepositoryImpl.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/StreamRepositoryImpl.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/handlers/StreamExceptionHandler.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/utils/Base64.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/utils/FeedFilterUtils.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/utils/SignatureUtils.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/utils/StreamRepoUtils.java delete mode 100644 stream-repo-okhttp/src/main/java/io/getstream/client/okhttp/repo/utils/UriBuilder.java delete mode 100644 stream-repo-okhttp/src/main/resources/stream-java.info delete mode 100644 stream-repo-okhttp/src/test/java/io/getstream/client/okhttp/IntegrationTest.java delete mode 100644 stream-repo-okhttp/src/test/java/io/getstream/client/okhttp/PersonalizedIntegrationTest.java delete mode 100644 stream-repo-okhttp/src/test/java/io/getstream/client/okhttp/StreamClientImplTest.java delete mode 100644 stream-repo-okhttp/src/test/java/io/getstream/client/okhttp/repo/StreamPersonalizedRepositoryImplTest.java delete mode 100644 stream-repo-okhttp/src/test/java/io/getstream/client/okhttp/repo/handlers/StreamExceptionHandlerTest.java delete mode 100644 stream-repo-okhttp/src/test/java/io/getstream/client/okhttp/repo/utils/InfoUtilTest.java delete mode 100644 stream-repo-okhttp/src/test/java/io/getstream/client/okhttp/repo/utils/StreamRepoUtilsTest.java delete mode 100644 stream-repo-okhttp/src/test/java/io/getstream/client/okhttp/repo/utils/UriBuilderTest.java diff --git a/.gitignore b/.gitignore index cd968238..06ddf981 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,6 @@ -*.class -.idea -*.iml - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.ear - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -# Maven -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties +build/ +out/ +.gradle/ +gradle.properties +.idea/ +.syntastic_javac_config diff --git a/.java-version b/.java-version new file mode 100644 index 00000000..ba6a626d --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +oracle64-10.0.2 diff --git a/.travis/publish.sh b/.travis/publish.sh deleted file mode 100755 index 6f5021e6..00000000 --- a/.travis/publish.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# -# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo. -# -# Adapted from https://coderwall.com/p/9b_lfq and -# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/ - -SLUG="GetStream/stream-java" -JDK="oraclejdk8" -BRANCH="master" - -set -e - -if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then - echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'." -elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then - echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'." -elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo "Skipping snapshot deployment: was pull request." -elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then - echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'." -else - echo "Deploying snapshot..." - mvn clean deploy --settings=".travis/settings.xml" -Dmaven.test.skip=true - echo "Snapshot deployed!" -fi \ No newline at end of file diff --git a/.travis/settings.xml b/.travis/settings.xml deleted file mode 100644 index de371552..00000000 --- a/.travis/settings.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - ossrh - ${env.CI_DEPLOY_USERNAME} - ${env.CI_DEPLOY_PASSWORD} - - - \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8a25cc4d..00000000 --- a/LICENSE +++ /dev/null @@ -1,48 +0,0 @@ -Copyright (c) 2016-2017 Stream.io Inc, and individual contributors. - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted -provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this list of - conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials - provided with the distribution. - - 3. Neither the name of the copyright holder nor the names of its contributors may - be used to endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - -This software incorporates additional Open Source components. You can find the source code of these -open source projects along with license information below. - - Base64.java: - - https://github.com/android/platform_frameworks_base/blob/master/core/java/android/util/Base64.java - - Copyright (C) 2010 The Android Open Source Project - - Licensed 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. diff --git a/README.md b/README.md deleted file mode 100644 index 90694696..00000000 --- a/README.md +++ /dev/null @@ -1,181 +0,0 @@ -stream-java -=========== -[![Build Status](https://travis-ci.org/GetStream/stream-java.svg?branch=master)](https://travis-ci.org/GetStream/stream-java) - -[stream-java](https://github.com/GetStream/stream-java) is a Java client for [Stream](https://getstream.io/). - -You can sign up for a Stream account at https://getstream.io/get_started. - -The Stream's Java client come in two different flavours, you should decide which one to drag into your project. -Those two implementations differ according to the underlying library used to handle HTTP connections: - -- *stream-repo-apache* uses Apache HttpClient and we recommend it for backend applications. Apache HttpClient is a mature, reliable and rock-solid HTTP library. -- *stream-repo-okhttp* uses Square's OkHttp which is lightweight, powerful and mobile-oriented HTTP library. We recommend it for mobile application. - -### Installation - -If you decide to go for the *Apache HttpClient* implementation, add the following dependency to your pom.xml: - -```xml - - io.getstream.client - stream-repo-apache - 2.1.3 - -``` - -or in your build.gradle: - -```gradle -compile 'io.getstream.client:stream-repo-apache:2.1.3' -``` - -Instead, if you opted for the *OkHttp* implementation please add it to your pom.xml - -```xml - - io.getstream.client - stream-repo-okhttp - 2.1.3 - -``` - -or in your build.gradle: - -```gradle -compile 'io.getstream.client:stream-repo-okhttp:2.1.3' -``` - -In case you want to download the artifact and put it manually into your project, -you can download it from [here](https://github.com/GetStream/stream-java/releases). - -Snapshots of the development version are available in [Sonatype](https://oss.sonatype.org/content/repositories/snapshots/io/getstream/client/) snapshots repository. - -#### JDK / JVM version requirements - -This API Client project requires Java SE 8. - -See the [Travis configuration](.travis.yml) for details of how it is built, tested and packaged. - -### Full documentation - -Documentation for this Java client are available at the [Stream website](https://getstream.io/docs/?language=java). - -### Usage - -```java -/** - * Instantiate a new client to connect to us east API endpoint - * Find your API keys here https://getstream.io/dashboard/ - **/ - -StreamClient streamClient = new StreamClientImpl(new ClientConfiguration(), "", ""); -``` - -#### Create a new Feed - -```java -/* Instantiate a feed object */ -Feed feed = streamClient.newFeed("user", "1"); -``` - -#### Working with Activities - -```java -/* Create an activity service */ -FlatActivityServiceImpl flatActivityService = feed.newFlatActivityService(SimpleActivity.class); - -/* Get activities from 5 to 10 (using offset pagination) */ -FeedFilter filter = new FeedFilter.Builder().withLimit(5).withOffset(5).build(); -List activities = flatActivityService.getActivities(filter).getResults(); - -/* Filter on an id less than the given UUID */ -aid = "e561de8f-00f1-11e4-b400-0cc47a024be0"; -FeedFilter filter = new FeedFilter.Builder().withIdLowerThan(aid).withLimit(5).build(); -List activities = flatActivityService.getActivities(filter).getResults(); - -/* Create a new activity */ -SimpleActivity activity = new SimpleActivity(); -activity.setActor("user:1"); -activity.setObject("tweet:1"); -activity.setVerb("tweet"); -activity.setForeignId("tweet:1"); -SimpleActivity response = flatActivityService.addActivity(activity); - -/* Remove an activity by its id */ -feed.deleteActivity("e561de8f-00f1-11e4-b400-0cc47a024be0"); - -/* Remove activities by their foreign_id */ -feed.deleteActivityByForeignId("tweet:1"); -``` - -In case you want to add a single activity to multiple feeds, you can use the batch feature _addToMany_: - -```java -/* Batch adding activities to many feeds */ -flatActivityService.addActivityToMany(ImmutableList.of("user:1", "user:2").asList(), myActivity); -``` - -The API client allows you to send activities with custom field as well, you can find a -complete example [here](https://github.com/GetStream/stream-java/blob/master/stream-repo-apache/src/test/java/io/getstream/client/example/mixtype/MixedType.java) - -#### Follow and Unfollow - -```java -/* Follow another feed */ -feed.follow(flat", "42"); - -/* Stop following another feed */ -feed.unfollow(flat", "42"); - -/* Retrieve first 10 followers of a feed */ -FeedFilter filter = new FeedFilter.Builder().withLimit(10).build(); -List followingPaged = feed.getFollowing(filter); - -/* Retrieve the first 10 followed feeds */ -FeedFilter filter = new FeedFilter.Builder().withLimit(10).build(); -List followingPaged = feed.getFollowing(filter); -``` - -In case you want to send to Stream a long list of following relationships you can use the batch feature _followMany_: - -```java -/* Batch following many feeds */ -FollowMany followMany = new FollowMany.Builder() - .add("user:1", "user:2") - .add("user:1", "user:3") - .add("user:1", "user:4") - .add("user:2", "user:3") - .build(); -feed.followMany(followMany); - -``` - -#### Client token - -In order to generate a token for client side usage (e.g. JS client), you can use the following code: - -```java -/* Generating tokens for client side usage */ -String token = feed.getToken(); -``` - -#### Further references - -For more examples have a look [here](https://github.com/GetStream/stream-java/tree/master/stream-repo-apache/src/test/java/io/getstream/client/apache/example). - -Docs are available on [GetStream.io](http://getstream.io/docs/). - -Javadocs are available [here](https://getstream.github.io/stream-java/). - -### Credits & Contributors - -This project was originally contributed by [Alessandro Pieri](sirio7g), prior to him joining Stream as an employee. - -We continue to welcome pull requests from community members. - -### Copyright and License Information - -Copyright (c) 2016-2017 Stream.io Inc, and individual contributors. All rights reserved. - -See the file "LICENSE" for information on the history of this software, terms & conditions for usage, and a DISCLAIMER OF ALL WARRANTIES. diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..14de87a2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,131 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven' + id 'signing' + id "com.scuilion.syntastic" version "0.3.8" + id "com.prot.versioninfo" version "0.5" + id 'net.researchgate.release' version '2.6.0' +} + +group 'io.getstream' +version = '2.0.0-BETA' + +dependencies { + testCompile 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testCompile 'org.junit.jupiter:junit-jupiter-params:5.3.1' + testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.3.1' + testRuntime 'org.junit.vintage:junit-vintage-engine:5.3.1' + testCompile 'com.pholser:junit-quickcheck-core:0.8.1' + testCompile 'com.pholser:junit-quickcheck-generators:0.8.1' + + implementation 'com.google.guava:guava:26.0-jre' + + implementation 'com.squareup.okhttp3:okhttp:3.11.0' + + compile 'com.fasterxml.jackson.core:jackson-core:2.9.6' + compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.6' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + + compile 'com.auth0:java-jwt:3.4.0' +} + +compileJava { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 +} + +test { + useJUnitPlatform { + includeEngines 'junit-jupiter', 'junit-vintage' + } + testLogging { + exceptionFormat = 'full' + events 'standard_out', 'standard_error', "passed", "skipped", "failed" + } +} + +repositories { + jcenter() +} + +processResources { + filesMatching('stream-java2.info') { + expand(project.properties) + } +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from tasks.javadoc.destinationDir +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource +} + +artifacts { + archives jar + archives javadocJar + archives sourcesJar +} + +signing { + sign configurations.archives +} + +release { + git { + requireBranch = 'new-client' + } +} + +uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: sonatypeUsername, password: sonatypePassword) + } + + pom.project { + name 'stream-java2' + packaging 'jar' + url 'https://github.com/GetStream/stream-java' + + scm { + url 'scm:git@github.com:GetStream/stream-java.git' + connection 'scm:git@github.com:GetStream/stream-java.git' + developerConnection 'scm:git@github.com:GetStream/stream-java.git' + } + + licenses { + license { + name 'The 3-Clause BSD License' + url 'https://opensource.org/licenses/BSD-3-Clause' + distribution 'repo' + } + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + + developers { + developer { + id 'sirio7g' + name 'Alessandro Pieri' + } + developer { + id 'nekuromento' + name 'Max Klyga' + } + } + } + } + } +} diff --git a/data/test.jpg b/data/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fd87cca4645e9112b99f5bf669afeab6cf9b4403 GIT binary patch literal 1362 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index dbf15999..00000000 --- a/pom.xml +++ /dev/null @@ -1,331 +0,0 @@ - - - 4.0.0 - - io.getstream.client - stream-java - pom - 2.1.4-SNAPSHOT - - stream-java - stream-java is a Java client for http://getstream.io. - https://github.com/GetStream/stream-java - - - - The 3-Clause BSD License - https://opensource.org/licenses/BSD-3-Clause - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - - sirio7g - Alessandro Pieri - https://github.com/sirio7g - - - tbarbugli - Tommaso Barbugli - https://github.com/tbarbugli - - - - - scm:git:git@github.com:GetStream/stream-java.git - scm:git:git@github.com:GetStream/stream-java.git - git@github.com:GetStream/stream-java.git - HEAD - - - - stream-core - stream-repo-apache - stream-repo-okhttp - - - - UTF-8 - UTF-8 - 1.6.6 - 1.1.2 - 18.0 - 2.4.3 - 1.0 - 4.11 - 1.10.19 - 3.3.0 - 1.57 - - true - false - - - - - - org.slf4j - slf4j-api - ${slf4j-api.version} - - - ch.qos.logback - logback-classic - ${logback.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - com.google.guava - guava - ${guava.version} - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - org.tomitribe - tomitribe-http-signatures - ${tomitribe-http-signatures.version} - - - com.auth0 - java-jwt - ${java-jwt.version} - - - - - junit - junit - ${junit.version} - test - - - org.mockito - mockito-core - ${mockito.version} - test - - - com.github.tomakehurst - wiremock - ${wiremock.version} - test - - - standalone - - - org.mortbay.jetty - jetty - - - com.google.guava - guava - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-databind - - - org.apache.httpcomponents - httpclient - - - org.skyscreamer - jsonassert - - - xmlunit - xmlunit - - - com.jayway.jsonpath - json-path - - - net.sf.jopt-simple - jopt-simple - - - - - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - - - org.apache.maven.plugins - maven-release-plugin - 2.5 - - false - release - deploy - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.3 - - - aggregate - - aggregate - - site - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 - true - - ossrh - https://oss.sonatype.org/ - true - - - - maven-compiler-plugin - 2.3.2 - - 1.7 - 1.7 - - - - maven-release-plugin - 2.5 - - - org.apache.maven.plugins - maven-surefire-plugin - 2.19.1 - - ${skipTests} - - **/*IntegrationTest.java - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.19.1 - - ${skipTests} - ${skipITs} - - **/IntegrationTest.java - - - - - integration-test - - integration-test - verify - - - - - - - - - - release - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - - - - diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..a373e07d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user guide at https://docs.gradle.org/4.10.2/userguide/multi_project_builds.html + */ + +rootProject.name = 'stream-java-2' diff --git a/src/main/java/example/Example.java b/src/main/java/example/Example.java new file mode 100644 index 00000000..2f5296fd --- /dev/null +++ b/src/main/java/example/Example.java @@ -0,0 +1,326 @@ +package example; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import io.getstream.client.Client; +import io.getstream.client.FlatFeed; +import io.getstream.client.NotificationFeed; +import io.getstream.core.KeepHistory; +import io.getstream.core.Region; +import io.getstream.core.models.*; +import io.getstream.core.options.ActivityMarker; +import io.getstream.core.options.Filter; +import io.getstream.core.options.Pagination; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Map; + +//TODO: move this to an appropriate place +class Example { + private static final String apiKey = "gp6e8sxxzud6"; + private static final String secret = "7j7exnksc4nxy399fdxvjqyqsqdahax3nfgtp27pumpc7sfm9um688pzpxjpjbf2"; + + public static void main(String[] args) throws Exception { + Client client = Client.builder(apiKey, secret) + .build(); + + FlatFeed chris = client.flatFeed("user", "chris"); + // Add an Activity; message is a custom field - tip: you can add unlimited custom fields! + chris.addActivity(Activity.builder() + .actor("chris") + .verb("add") + .object("picture:10") + .foreignID("picture:10") + .extraField("message", "Beautiful bird!") + .build()); + + // Create a following relationship between Jack's "timeline" feed and Chris' "user" feed: + FlatFeed jack = client.flatFeed("timeline", "jack"); + jack.follow(chris); + + // Read Jack's timeline and Chris' post appears in the feed: + List response = jack.getActivities(new Pagination().limit(10)).join(); + for (Activity activity : response) { + // ... + } + + // Remove an Activity by referencing it's foreign_id + chris.removeActivityByForeignID("picture:10"); + + /* -------------------------------------------------------- */ + + // Instantiate a feed object + FlatFeed userFeed = client.flatFeed("user", "1"); + + // Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`) + Activity activity = Activity.builder() + .actor("User:1") + .verb("pin") + .object("Place:42") + .target("Board:1") + .build(); + userFeed.addActivity(activity); + + // Create a bit more complex activity + activity = Activity.builder() + .actor("User:1") + .verb("run") + .object("Exercise:42") + .foreignID("run:1") + .extra(new ImmutableMap.Builder() + .put("course", new ImmutableMap.Builder() + .put("name", "Golden Gate park") + .put("distance", 10) + .build()) + .put("participants", new String[]{ + "Thierry", + "Tommaso", + }) + .put("started_at", LocalDateTime.now()) + .put("location", new ImmutableMap.Builder() + .put("type", "point") + .put("coordinates", new double[]{37.769722, -122.476944}) + .build()) + .build()) + .build(); + userFeed.addActivity(activity); + + // Remove an activity by its id + userFeed.removeActivityByID("e561de8f-00f1-11e4-b400-0cc47a024be0"); + + // Remove activities with foreign_id 'run:1' + userFeed.removeActivityByForeignID("run:1"); + + activity = Activity.builder() + .actor("1") + .verb("like") + .object("3") + .time(new Date()) + .foreignID("like:3") + .extraField("popularity", 100) + .build(); + + // first time the activity is added + userFeed.addActivity(activity); + + // update the popularity value for the activity + activity = Activity.builder() + .fromActivity(activity) + .extraField("popularity", 10) + .build(); + + client.batch().updateActivities(activity); + + /* -------------------------------------------------------- */ + + // partial update by activity ID + + // prepare the set operations + Map set = new ImmutableMap.Builder() + .put("product.price", 19.99) + .put("shares", new ImmutableMap.Builder() + .put("facebook", "...") + .put("twitter", "...") + .build()) + .build(); + // prepare the unset operations + String[] unset = new String[] { "daily_likes", "popularity" }; + + String id = "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4"; + client.updateActivityByID(id, set, unset); + + String foreignID = "product:123"; + Date timestamp = new Date(); + client.updateActivityByForeignID(foreignID, timestamp, set, unset); + + FeedID[] add = new FeedID[0]; + FeedID[] remove = new FeedID[0]; + userFeed.updateActivityToTargets(activity, add, remove); + + FeedID[] newTargets = new FeedID[0]; + userFeed.replaceActivityToTargets(activity, newTargets); + + /* -------------------------------------------------------- */ + + Date now = new Date(); + Activity firstActivity = userFeed.addActivity(Activity.builder() + .actor("1") + .verb("like") + .object("3") + .time(now) + .foreignID("like:3") + .build()).join(); + Activity secondActivity = userFeed.addActivity(Activity.builder() + .actor("1") + .verb("like") + .object("3") + .time(now) + .extraField("extra", "extra_value") + .foreignID("like:3") + .build()).join(); + // foreign ID and time are the same for both activities + // hence only one activity is created and first and second IDs are equal + // firstActivity.ID == secondActivity.ID + + /* -------------------------------------------------------- */ + + // Get 5 activities with id less than the given UUID (Faster - Recommended!) + response = userFeed.getActivities(new Pagination().limit(5), new Filter().idLessThan("e561de8f-00f1-11e4-b400-0cc47a024be0")).join(); + // Get activities from 5 to 10 (Pagination-based - Slower) + response = userFeed.getActivities(new Pagination().offset(0).limit(5)).join(); + // Get activities sorted by rank (Ranked Feeds Enabled): + response = userFeed.getActivities(new Pagination().limit(5), "popularity").join(); + + /* -------------------------------------------------------- */ + + // timeline:timeline_feed_1 follows user:user_42 + FlatFeed user = client.flatFeed("user", "user_42"); + FlatFeed timeline = client.flatFeed("timeline", "timeline_feed_1"); + timeline.follow(user); + + // follow feed without copying the activities: + timeline.follow(user, 0); + + /* -------------------------------------------------------- */ + + // user := client.FlatFeed("user", "42") + + // Stop following feed user:user_42 + timeline.unfollow(user); + + // Stop following feed user:user_42 but keep history of activities + timeline.unfollow(user, KeepHistory.YES); + + // list followers + List followers = userFeed.getFollowers(new Pagination().offset(0).limit(10)).join(); + for (FollowRelation follow : followers) { + System.out.format("%s -> %s", follow.getSource(), follow.getTarget()); + // ... + } + + // Retrieve last 10 feeds followed by user_feed_1 + List followed = userFeed.getFollowed(new Pagination().offset(0).limit(10)).join(); + + // Retrieve 10 feeds followed by user_feed_1 starting from the 11th + followed = userFeed.getFollowed(new Pagination().offset(10).limit(10)).join(); + + // Check if user_feed_1 follows specific feeds + followed = userFeed.getFollowed(new Pagination().offset(0).limit(2), new FeedID("user:42"), new FeedID("user", "43")).join(); + + /* -------------------------------------------------------- */ + + NotificationFeed notifications = client.notificationFeed("notifications", "1"); + // Mark all activities in the feed as seen + List> activityGroups = notifications.getActivities(new ActivityMarker().allSeen()).join(); + for (NotificationGroup group : activityGroups) { + // ... + } + // Mark some activities as read via specific Activity Group Ids + activityGroups = notifications.getActivities(new ActivityMarker().read("groupID1", "groupID2" /* ... */)).join(); + + /* -------------------------------------------------------- */ + + // Add an activity to the feed, where actor, object and target are references to objects - adding your ranking method as a parameter (in this case, "popularity"): + activity = Activity.builder() + .actor("User:1") + .verb("pin") + .object("place:42") + .target("board:1") + .extraField("popularity", 5) + .build(); + userFeed.addActivity(activity); + + // Get activities sorted by the ranking method labelled 'activity_popularity' (Ranked Feeds Enabled) + response = userFeed.getActivities(new Pagination().limit(5), "activity_popularity").join(); + + /* -------------------------------------------------------- */ + + // Add the activity to Eric's feed and to Jessica's notification feed + activity = Activity.builder() + .actor("User:Eric") + .verb("tweet") + .object("tweet:id") + .to(Lists.newArrayList(new FeedID("notification:Jessica"))) + .extraField("message", "@Jessica check out getstream.io it's so dang awesome.") + .build(); + userFeed.addActivity(activity); + + // The TO field ensures the activity is send to the player, match and team feed + activity = Activity.builder() + .actor("Player:Suarez") + .verb("foul") + .object("Player:Ramos") + .to(Lists.newArrayList(new FeedID("team:barcelona"), new FeedID("match:1"))) + .extraField("match", ImmutableMap.of("El Classico", 10)) + .build(); + // playerFeed.addActivity(activity); + userFeed.addActivity(activity); + + /* -------------------------------------------------------- */ + + // Batch following many feeds + // Let timeline:1 will follow user:1, user:2 and user:3 + FollowRelation[] follows = new FollowRelation[]{ + new FollowRelation("timeline:1", "user:1"), + new FollowRelation("timeline:3", "user:2"), + new FollowRelation("timeline:1", "user:3") + }; + client.batch().followMany(follows); + // copy only the last 10 activities from every feed + client.batch().followMany(10, follows); + + /* -------------------------------------------------------- */ + + Activity[] activities = new Activity[]{ + Activity.builder() + .actor("User:1") + .verb("tweet") + .object("Tweet:1") + .build(), + Activity.builder() + .actor("User:2") + .verb("watch") + .object("Movie:1") + .build() + }; + userFeed.addActivities(activities); + + /* -------------------------------------------------------- */ + + // adds 1 activity to many feeds in one request + activity = Activity.builder() + .actor("User:2") + .verb("pin") + .object("Place:42") + .target("Board:1") + .build(); + FeedID[] feeds = new FeedID[]{ + new FeedID("timeline", "1"), + new FeedID("timeline", "2"), + new FeedID("timeline", "3"), + new FeedID("timeline", "4") + }; + client.batch().addToMany(activity, feeds); + + /* -------------------------------------------------------- */ + + // retrieve two activities by ID + client.batch().getActivitiesByID("01b3c1dd-e7ab-4649-b5b3-b4371d8f7045", "ed2837a6-0a3b-4679-adc1-778a1704852"); + + // retrieve an activity by foreign ID and time + client.batch().getActivitiesByForeignID(new ForeignIDTimePair("foreignID1", new Date()), new ForeignIDTimePair("foreignID2", new Date())); + + /* -------------------------------------------------------- */ + + // connect to the us-east region + client = Client.builder(apiKey, secret) + .region(Region.US_EAST) + .build(); + + /* -------------------------------------------------------- */ + + + } +} diff --git a/src/main/java/io/getstream/client/AggregatedFeed.java b/src/main/java/io/getstream/client/AggregatedFeed.java new file mode 100644 index 00000000..fe0fb6fc --- /dev/null +++ b/src/main/java/io/getstream/client/AggregatedFeed.java @@ -0,0 +1,184 @@ +package io.getstream.client; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import io.getstream.core.models.EnrichedActivity; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.Group; +import io.getstream.core.options.ActivityMarker; +import io.getstream.core.options.Filter; +import io.getstream.core.options.Pagination; +import io.getstream.core.utils.DefaultOptions; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static io.getstream.core.utils.Serialization.deserializeContainer; + +public class AggregatedFeed extends Feed { + AggregatedFeed(Client client, FeedID id) { + super(client, id); + } + + public CompletableFuture>> getActivities() throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getActivities(Pagination pagination) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getActivities(Filter filter) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getActivities(ActivityMarker marker) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getActivities(Filter filter, ActivityMarker marker) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + public CompletableFuture>> getActivities(Pagination pagination, ActivityMarker marker) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getActivities(Pagination pagination, Filter filter) throws StreamException { + return getActivities(pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getActivities(Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, Group.class, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture>> getCustomActivities(Class type) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getCustomActivities(Class type, Filter filter) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getCustomActivities(Class type, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getCustomActivities(Class type, Filter filter, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getCustomActivities(type, pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, Group.class, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture>> getEnrichedActivities() throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedActivities(Pagination pagination) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedActivities(Filter filter) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedActivities(ActivityMarker marker) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getEnrichedActivities(Filter filter, ActivityMarker marker) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + public CompletableFuture>> getEnrichedActivities(Pagination pagination, ActivityMarker marker) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getEnrichedActivities(Pagination pagination, Filter filter) throws StreamException { + return getEnrichedActivities(pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedActivities(Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getEnrichedActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, Group.class, EnrichedActivity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Filter filter, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getEnrichedActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, Group.class, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/client/AnalyticsClient.java b/src/main/java/io/getstream/client/AnalyticsClient.java new file mode 100644 index 00000000..742d47d7 --- /dev/null +++ b/src/main/java/io/getstream/client/AnalyticsClient.java @@ -0,0 +1,56 @@ +package io.getstream.client; + +import com.google.common.collect.Iterables; +import io.getstream.core.StreamAnalytics; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.models.Engagement; +import io.getstream.core.models.Impression; +import io.getstream.core.utils.Auth.TokenAction; + +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import static io.getstream.core.utils.Auth.buildAnalyticsRedirectToken; +import static io.getstream.core.utils.Auth.buildAnalyticsToken; + +public final class AnalyticsClient { + private final String secret; + private final StreamAnalytics analytics; + + AnalyticsClient(String secret, StreamAnalytics analytics) { + this.secret = secret; + this.analytics = analytics; + } + + public CompletableFuture trackEngagement(Iterable events) throws StreamException { + return trackEngagement(Iterables.toArray(events, Engagement.class)); + } + + public CompletableFuture trackEngagement(Engagement... events) throws StreamException { + final Token token = buildAnalyticsToken(secret, TokenAction.WRITE); + return analytics.trackEngagement(token, events); + } + + public CompletableFuture trackImpression(Impression event) throws StreamException { + final Token token = buildAnalyticsToken(secret, TokenAction.WRITE); + return analytics.trackImpression(token, event); + } + + public URL createRedirectURL(URL url, Engagement... engagements) throws StreamException { + return createRedirectURL(url, new Impression[0], engagements); + } + + public URL createRedirectURL(URL url, Impression... impressions) throws StreamException { + return createRedirectURL(url, impressions, new Engagement[0]); + } + + public URL createRedirectURL(URL url, Iterable impressions, Iterable engagements) throws StreamException { + return createRedirectURL(url, Iterables.toArray(impressions, Impression.class), Iterables.toArray(engagements, Engagement.class)); + } + + public URL createRedirectURL(URL url, Impression[] impressions, Engagement[] engagements) throws StreamException { + final Token token = buildAnalyticsRedirectToken(secret); + return analytics.createRedirectURL(token, url, impressions, engagements); + } +} diff --git a/src/main/java/io/getstream/client/BatchClient.java b/src/main/java/io/getstream/client/BatchClient.java new file mode 100644 index 00000000..7cb05333 --- /dev/null +++ b/src/main/java/io/getstream/client/BatchClient.java @@ -0,0 +1,80 @@ +package io.getstream.client; + +import com.google.common.collect.Iterables; +import io.getstream.core.StreamBatch; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.models.Activity; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.FollowRelation; +import io.getstream.core.models.ForeignIDTimePair; +import io.getstream.core.utils.DefaultOptions; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static io.getstream.core.utils.Auth.*; + +public final class BatchClient { + private final String secret; + private final StreamBatch batch; + + BatchClient(String secret, StreamBatch batch) { + this.secret = secret; + this.batch = batch; + } + + public CompletableFuture addToMany(Activity activity, FeedID... feeds) throws StreamException { + final Token token = buildFeedToken(secret, TokenAction.WRITE); + return batch.addToMany(token, activity, feeds); + } + + public CompletableFuture followMany(int activityCopyLimit, FollowRelation... follows) throws StreamException { + final Token token = buildFollowToken(secret, TokenAction.WRITE); + return batch.followMany(token, activityCopyLimit, follows); + } + + public CompletableFuture followMany(int activityCopyLimit, Iterable follows) throws StreamException { + return followMany(activityCopyLimit, Iterables.toArray(follows, FollowRelation.class)); + } + + public CompletableFuture followMany(FollowRelation... follows) throws StreamException { + return followMany(DefaultOptions.DEFAULT_ACTIVITY_COPY_LIMIT, follows); + } + + public CompletableFuture followMany(Iterable follows) throws StreamException { + return followMany(Iterables.toArray(follows, FollowRelation.class)); + } + + public CompletableFuture unfollowMany(FollowRelation... follows) throws StreamException { + final Token token = buildFollowToken(secret, TokenAction.DELETE); + return batch.unfollowMany(token, follows); + } + + public CompletableFuture> getActivitiesByID(Iterable activityIDs) throws StreamException { + return getActivitiesByID(Iterables.toArray(activityIDs, String.class)); + } + + public CompletableFuture> getActivitiesByID(String... activityIDs) throws StreamException { + final Token token = buildActivityToken(secret, TokenAction.READ); + return batch.getActivitiesByID(token, activityIDs); + } + + public CompletableFuture> getActivitiesByForeignID(Iterable activityIDTimePairs) throws StreamException { + return getActivitiesByForeignID(Iterables.toArray(activityIDTimePairs, ForeignIDTimePair.class)); + } + + public CompletableFuture> getActivitiesByForeignID(ForeignIDTimePair... activityIDTimePairs) throws StreamException { + final Token token = buildActivityToken(secret, TokenAction.READ); + return batch.getActivitiesByForeignID(token, activityIDTimePairs); + } + + public CompletableFuture updateActivities(Iterable activities) throws StreamException { + return updateActivities(Iterables.toArray(activities, Activity.class)); + } + + public CompletableFuture updateActivities(Activity... activities) throws StreamException { + final Token token = buildActivityToken(secret, TokenAction.WRITE); + return batch.updateActivities(token, activities); + } +} diff --git a/src/main/java/io/getstream/client/Client.java b/src/main/java/io/getstream/client/Client.java new file mode 100644 index 00000000..77d41a1e --- /dev/null +++ b/src/main/java/io/getstream/client/Client.java @@ -0,0 +1,296 @@ +package io.getstream.client; + +import com.google.common.collect.Iterables; +import io.getstream.core.Region; +import io.getstream.core.Stream; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.OKHTTPClientAdapter; +import io.getstream.core.http.Response; +import io.getstream.core.http.Token; +import io.getstream.core.models.*; +import io.getstream.core.options.RequestOption; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Auth.*; + +public final class Client { + private final String secret; + private final Stream stream; + + private Client(String key, String secret, URL baseURL, HTTPClient httpClient) { + this.secret = secret; + this.stream = new Stream(key, baseURL, httpClient); + } + + public static Builder builder(String apiKey, String secret) { + return new Builder(apiKey, secret); + } + + public CompletableFuture updateActivityByID(String id, Map set, Iterable unset) throws StreamException { + return updateActivityByID(id, set, Iterables.toArray(unset, String.class)); + } + + public CompletableFuture updateActivityByID(String id, Map set, String[] unset) throws StreamException { + final Token token = buildFeedToken(secret, TokenAction.WRITE); + return stream.updateActivityByID(token, id, set, unset); + } + + public CompletableFuture updateActivityByForeignID(ForeignIDTimePair foreignIDTimePair, Map set, Iterable unset) throws StreamException { + checkNotNull(foreignIDTimePair, "No activity to update"); + return updateActivityByForeignID(foreignIDTimePair.getForeignID(), foreignIDTimePair.getTime(), set, unset); + } + + public CompletableFuture updateActivityByForeignID(ForeignIDTimePair foreignIDTimePair, Map set, String[] unset) throws StreamException { + checkNotNull(foreignIDTimePair, "No activity to update"); + return updateActivityByForeignID(foreignIDTimePair.getForeignID(), foreignIDTimePair.getTime(), set, unset); + } + + public CompletableFuture updateActivityByForeignID(String foreignID, Date timestamp, Map set, Iterable unset) throws StreamException { + return updateActivityByForeignID(foreignID, timestamp, set, Iterables.toArray(unset, String.class)); + } + + public CompletableFuture updateActivityByForeignID(String foreignID, Date timestamp, Map set, String[] unset) throws StreamException { + final Token token = buildActivityToken(secret, TokenAction.WRITE); + return stream.updateActivityByForeignID(token, foreignID, timestamp, set, unset); + } + + public CompletableFuture openGraph(URL url) throws StreamException { + final Token token = buildOpenGraphToken(secret); + return stream.openGraph(token, url); + } + + public static final class Builder { + private static final String DEFAULT_HOST = "stream-io-api.com"; + + private final String apiKey; + private final String secret; + private HTTPClient httpClient; + + private String scheme = "https"; + private String region = Region.US_EAST.toString(); + private String host = DEFAULT_HOST; + private int port = 443; + + public Builder(String apiKey, String secret) { + checkNotNull(apiKey, "API key can't be null"); + checkNotNull(secret, "Secret can't be null"); + checkArgument(!apiKey.isEmpty(), "API key can't be empty"); + checkArgument(!secret.isEmpty(), "Secret can't be empty"); + this.apiKey = apiKey; + this.secret = secret; + } + + public Builder httpClient(HTTPClient httpClient) { + checkNotNull(httpClient, "HTTP client can't be null"); + this.httpClient = httpClient; + return this; + } + + public Builder scheme(String scheme) { + checkNotNull(scheme, "Scheme can't be null"); + checkArgument(!scheme.isEmpty(), "Scheme can't be empty"); + this.scheme = scheme; + return this; + } + + public Builder host(String host) { + checkNotNull(host, "Host can't be null"); + checkArgument(!host.isEmpty(), "Host can't be empty"); + this.host = host; + return this; + } + + public Builder port(int port) { + checkArgument(port > 0, "Port has to be a non-zero positive number"); + this.port = port; + return this; + } + + public Builder region(Region region) { + checkNotNull(region, "Region can't be null"); + this.region = region.toString(); + return this; + } + + public Builder region(String region) { + checkNotNull(region, "Region can't be null"); + checkArgument(!region.isEmpty(), "Region can't be empty"); + this.region = region; + return this; + } + + private String buildHost() { + final StringBuilder sb = new StringBuilder(); + if (host.equals(DEFAULT_HOST)) { + sb.append(region).append("."); + } + sb.append(host); + return sb.toString(); + } + + public Client build() throws MalformedURLException { + if (httpClient == null) { + httpClient = new OKHTTPClientAdapter(); + } + return new Client(apiKey, secret, new URL(scheme, buildHost(), port, ""), httpClient); + } + } + + public T getHTTPClientImplementation() { + return stream.getHTTPClientImplementation(); + } + + public Token frontendToken(String userID) { + return buildFrontendToken(secret, userID); + } + + public FlatFeed flatFeed(FeedID id) { + return new FlatFeed(this, id); + } + + public FlatFeed flatFeed(String slug, String userID) { + return flatFeed(new FeedID(slug, userID)); + } + + public AggregatedFeed aggregatedFeed(FeedID id) { + return new AggregatedFeed(this, id); + } + + public AggregatedFeed aggregatedFeed(String slug, String userID) { + return aggregatedFeed(new FeedID(slug, userID)); + } + + public NotificationFeed notificationFeed(FeedID id) { + return new NotificationFeed(this, id); + } + + public NotificationFeed notificationFeed(String slug, String userID) { + return notificationFeed(new FeedID(slug, userID)); + } + + public User user(String userID) { + return new User(this, userID); + } + + public BatchClient batch() { + return new BatchClient(secret, stream.batch()); + } + + public CollectionsClient collections() { + return new CollectionsClient(secret, stream.collections()); + } + + public PersonalizationClient personalization() { + return new PersonalizationClient(secret, stream.personalization()); + } + + public AnalyticsClient analytics() { + return new AnalyticsClient(secret, stream.analytics()); + } + + public ReactionsClient reactions() { + return new ReactionsClient(secret, stream.reactions()); + } + + public FileStorageClient files() { + return new FileStorageClient(secret, stream.files()); + } + + public ImageStorageClient images() { + return new ImageStorageClient(secret, stream.images()); + } + + CompletableFuture getActivities(FeedID feed, RequestOption... options) throws StreamException { + final Token token = buildFeedToken(secret, feed, TokenAction.READ); + return stream.getActivities(token, feed, options); + } + + CompletableFuture getEnrichedActivities(FeedID feed, RequestOption... options) throws StreamException { + final Token token = buildFeedToken(secret, feed, TokenAction.READ); + return stream.getEnrichedActivities(token, feed, options); + } + + CompletableFuture addActivity(FeedID feed, Activity activity) throws StreamException { + final Token token = buildFeedToken(secret, feed, TokenAction.WRITE); + return stream.addActivity(token, feed, activity); + } + + CompletableFuture addActivities(FeedID feed, Activity... activities) throws StreamException { + final Token token = buildFeedToken(secret, feed, TokenAction.WRITE); + return stream.addActivities(token, feed, activities); + } + + CompletableFuture removeActivityByID(FeedID feed, String id) throws StreamException { + final Token token = buildFeedToken(secret, feed, TokenAction.DELETE); + return stream.removeActivityByID(token, feed, id); + } + + CompletableFuture removeActivityByForeignID(FeedID feed, String foreignID) throws StreamException { + final Token token = buildFeedToken(secret, feed, TokenAction.DELETE); + return stream.removeActivityByForeignID(token, feed, foreignID); + } + + CompletableFuture follow(FeedID source, FeedID target, int activityCopyLimit) throws StreamException { + final Token token = buildFollowToken(secret, source, TokenAction.WRITE); + final Token targetToken = buildFeedToken(secret, target, TokenAction.READ); + return stream.follow(token, targetToken, source, target, activityCopyLimit); + } + + CompletableFuture getFollowers(FeedID feed, RequestOption... options) throws StreamException { + final Token token = buildFollowToken(secret, feed, TokenAction.READ); + return stream.getFollowers(token, feed, options); + } + + CompletableFuture getFollowed(FeedID feed, RequestOption... options) throws StreamException { + final Token token = buildFollowToken(secret, feed, TokenAction.READ); + return stream.getFollowed(token, feed, options); + } + + CompletableFuture unfollow(FeedID source, FeedID target, RequestOption... options) throws StreamException { + final Token token = buildFollowToken(secret, source, TokenAction.DELETE); + return stream.unfollow(token, source, target, options); + } + + CompletableFuture updateActivityToTargets(FeedID feed, Activity activity, FeedID[] add, FeedID[] remove, FeedID[] newTargets) throws StreamException { + final Token token = buildToTargetUpdateToken(secret, feed, TokenAction.WRITE); + return stream.updateActivityToTargets(token, feed, activity, add, remove, newTargets); + } + + CompletableFuture getUser(String id) throws StreamException { + final Token token = buildUsersToken(secret, TokenAction.READ); + return stream.getUser(token, id, false); + } + + CompletableFuture deleteUser(String id) throws StreamException { + final Token token = buildUsersToken(secret, TokenAction.DELETE); + return stream.deleteUser(token, id); + } + + CompletableFuture getOrCreateUser(String id, Data data) throws StreamException { + final Token token = buildUsersToken(secret, TokenAction.WRITE); + return stream.createUser(token, id, data, true); + } + + CompletableFuture createUser(String id, Data data) throws StreamException { + final Token token = buildUsersToken(secret, TokenAction.WRITE); + return stream.createUser(token, id, data, false); + } + + CompletableFuture updateUser(String id, Data data) throws StreamException { + final Token token = buildUsersToken(secret, TokenAction.WRITE); + return stream.updateUser(token, id, data); + } + + CompletableFuture userProfile(String id) throws StreamException { + final Token token = buildUsersToken(secret, TokenAction.READ); + return stream.getUser(token, id, true); + } +} diff --git a/src/main/java/io/getstream/client/CollectionsClient.java b/src/main/java/io/getstream/client/CollectionsClient.java new file mode 100644 index 00000000..9b2808ec --- /dev/null +++ b/src/main/java/io/getstream/client/CollectionsClient.java @@ -0,0 +1,139 @@ +package io.getstream.client; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Streams; +import io.getstream.core.StreamCollections; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.models.CollectionData; +import io.getstream.core.utils.Auth.TokenAction; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import static io.getstream.core.utils.Auth.buildCollectionsToken; +import static io.getstream.core.utils.Serialization.convert; + +public final class CollectionsClient { + private final String secret; + private final StreamCollections collections; + + CollectionsClient(String secret, StreamCollections collections) { + this.secret = secret; + this.collections = collections; + } + + public CompletableFuture addCustom(String collection, T item) throws StreamException { + return addCustom(null, collection, item); + } + + public CompletableFuture addCustom(String userID, String collection, T item) throws StreamException { + return add(userID, collection, convert(item, CollectionData.class)) + .thenApply(data -> convert(data, (Class) item.getClass())); + } + + public CompletableFuture add(String collection, CollectionData item) throws StreamException { + return add(null, collection, item); + } + + public CompletableFuture add(String userID, String collection, CollectionData item) throws StreamException { + final Token token = buildCollectionsToken(secret, TokenAction.WRITE); + return collections.add(token, userID, collection, item); + } + + public CompletableFuture updateCustom(String collection, T item) throws StreamException { + return updateCustom(null, collection, item); + } + + public CompletableFuture updateCustom(String userID, String collection, T item) throws StreamException { + return update(userID, collection, convert(item, CollectionData.class)) + .thenApply(data -> convert(data, (Class) item.getClass())); + } + + public CompletableFuture update(String collection, CollectionData item) throws StreamException { + return update(null, collection, item); + } + + public CompletableFuture update(String userID, String collection, CollectionData item) throws StreamException { + final Token token = buildCollectionsToken(secret, TokenAction.WRITE); + return collections.update(token, userID, collection, item); + } + + public CompletableFuture upsertManyCustom(String collection, Iterable items) throws StreamException { + final CollectionData[] custom = Streams.stream(items) + .map(item -> CollectionData.buildFrom(item)) + .toArray(CollectionData[]::new); + return upsertMany(collection, custom); + } + + public CompletableFuture upsertManyCustom(String collection, T... items) throws StreamException { + final CollectionData[] custom = Arrays.stream(items) + .map(item -> CollectionData.buildFrom(item)) + .toArray(CollectionData[]::new); + return upsertMany(collection, custom); + } + + public CompletableFuture upsertMany(String collection, Iterable items) throws StreamException { + return upsertMany(collection, Iterables.toArray(items, CollectionData.class)); + } + + public CompletableFuture upsertMany(String collection, CollectionData... items) throws StreamException { + final Token token = buildCollectionsToken(secret, TokenAction.WRITE); + return collections.upsertMany(token, collection, items); + } + + public CompletableFuture> customItems(Class type, String collection) throws StreamException { + return items(collection) + .thenApply(result -> result.stream() + .map(item -> convert(item, type)) + .collect(Collectors.toList())); + } + + public CompletableFuture> items(String collection) throws StreamException { + final Token token = buildCollectionsToken(secret, TokenAction.READ); + return collections.items(token, collection); + } + + public CompletableFuture getCustom(Class type, String collection, String id) throws StreamException { + return get(collection, id).thenApply(data -> convert(data, type)); + } + + public CompletableFuture get(String collection, String id) throws StreamException { + final Token token = buildCollectionsToken(secret, TokenAction.READ); + return collections.get(token, collection, id); + } + + public CompletableFuture> getManyCustom(Class type, String collection, Iterable ids) throws StreamException { + return getManyCustom(type, collection, Iterables.toArray(ids, String.class)); + } + + public CompletableFuture> getManyCustom(Class type, String collection, String... ids) throws StreamException { + return getMany(collection, ids) + .thenApply(data -> data.stream().map(item -> convert(item, type)).collect(Collectors.toList())); + } + + public CompletableFuture> getMany(String collection, Iterable ids) throws StreamException { + return getMany(collection, Iterables.toArray(ids, String.class)); + } + + public CompletableFuture> getMany(String collection, String... ids) throws StreamException { + final Token token = buildCollectionsToken(secret, TokenAction.READ); + return collections.getMany(token, collection, ids); + } + + public CompletableFuture delete(String collection, String id) throws StreamException { + final Token token = buildCollectionsToken(secret, TokenAction.DELETE); + return collections.delete(token, collection, id); + } + + public CompletableFuture deleteMany(String collection, Iterable ids) throws StreamException { + return deleteMany(collection, Iterables.toArray(ids, String.class)); + } + + public CompletableFuture deleteMany(String collection, String... ids) throws StreamException { + final Token token = buildCollectionsToken(secret, TokenAction.DELETE); + return collections.deleteMany(token, collection, ids); + } +} diff --git a/src/main/java/io/getstream/client/Feed.java b/src/main/java/io/getstream/client/Feed.java new file mode 100644 index 00000000..3ab25599 --- /dev/null +++ b/src/main/java/io/getstream/client/Feed.java @@ -0,0 +1,278 @@ +package io.getstream.client; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Streams; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.FollowRelation; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.Pagination; +import io.getstream.core.options.RequestOption; +import io.getstream.core.utils.DefaultOptions; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.*; + +public class Feed { + private final Client client; + private final FeedID id; + + Feed(Client client, FeedID id) { + checkNotNull(client, "Can't create feed w/o a client"); + checkNotNull(id, "Can't create feed w/o an ID"); + + this.client = client; + this.id = id; + } + + protected final Client getClient() { + return client; + } + + public final FeedID getID() { + return id; + } + + public final String getSlug() { + return id.getSlug(); + } + + public final String getUserID() { + return id.getUserID(); + } + + public final CompletableFuture addActivity(Activity activity) throws StreamException { + return getClient() + .addActivity(id, activity) + .thenApply(response -> { + try { + return deserialize(response, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture addCustomActivity(T activity) throws StreamException { + return getClient() + .addActivity(id, Activity.builder().fromCustomActivity(activity).build()) + .thenApply(response -> { + try { + return deserialize(response, (Class) activity.getClass()); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> addActivities(Iterable activities) throws StreamException { + return addActivities(Iterables.toArray(activities, Activity.class)); + } + + public final CompletableFuture> addCustomActivities(Iterable activities) throws StreamException { + final Activity[] custom = Streams.stream(activities) + .map(activity -> Activity.builder().fromCustomActivity(activity).build()) + .toArray(Activity[]::new); + return getClient() + .addActivities(id, custom) + .thenApply(response -> { + try { + Class element = (Class) ((ParameterizedType) getClass().getGenericSuperclass()) .getActualTypeArguments()[0]; + return deserializeContainer(response, "activities", element); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> addActivities(Activity... activities) throws StreamException { + return getClient() + .addActivities(id, activities) + .thenApply(response -> { + try { + return deserializeContainer(response, "activities", Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> addCustomActivities(T... activities) throws StreamException { + final Activity[] custom = Arrays.stream(activities) + .map(activity -> Activity.builder().fromCustomActivity(activity).build()) + .toArray(Activity[]::new); + return getClient() + .addActivities(id, custom) + .thenApply(response -> { + try { + Class element = (Class) activities.getClass().getComponentType(); + return deserializeContainer(response, "activities", element); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture removeActivityByID(String id) throws StreamException { + return client + .removeActivityByID(this.id, id) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture removeActivityByForeignID(String foreignID) throws StreamException { + return client + .removeActivityByForeignID(id, foreignID) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture follow(FlatFeed feed) throws StreamException { + return follow(feed, DefaultOptions.DEFAULT_ACTIVITY_COPY_LIMIT); + } + + public final CompletableFuture follow(FlatFeed feed, int activityCopyLimit) throws StreamException { + checkArgument(activityCopyLimit <= DefaultOptions.MAX_ACTIVITY_COPY_LIMIT, String.format("Activity copy limit should be less then %d", DefaultOptions.MAX_ACTIVITY_COPY_LIMIT)); + + return client + .follow(id, feed.getID(), activityCopyLimit) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> getFollowers(Iterable feedIDs) throws StreamException { + return getFollowers(DefaultOptions.DEFAULT_PAGINATION, Iterables.toArray(feedIDs, FeedID.class)); + } + + public final CompletableFuture> getFollowers(FeedID... feedIDs) throws StreamException { + return getFollowers(DefaultOptions.DEFAULT_PAGINATION, feedIDs); + } + + public final CompletableFuture> getFollowers(Pagination pagination, Iterable feedIDs) throws StreamException { + return getFollowers(pagination, Iterables.toArray(feedIDs, FeedID.class)); + } + + public final CompletableFuture> getFollowers(Pagination pagination, FeedID... feeds) throws StreamException { + checkNotNull(feeds, "No feed ids to filter on"); + + final String[] feedIDs = Arrays.stream(feeds) + .map(id -> id.toString()) + .toArray(String[]::new); + final RequestOption[] options = feedIDs.length == 0 + ? new RequestOption[] { pagination } + : new RequestOption[] { pagination, new CustomQueryParameter("filter", String.join(",", feedIDs)) }; + return client + .getFollowers(id, options) + .thenApply(response -> { + try { + return deserializeContainer(response, FollowRelation.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> getFollowed(Iterable feedIDs) throws StreamException { + return getFollowed(DefaultOptions.DEFAULT_PAGINATION, Iterables.toArray(feedIDs, FeedID.class)); + } + + public final CompletableFuture> getFollowed(FeedID... feedIDs) throws StreamException { + return getFollowed(DefaultOptions.DEFAULT_PAGINATION, feedIDs); + } + + public final CompletableFuture> getFollowed(Pagination pagination, Iterable feedIDs) throws StreamException { + return getFollowed(pagination, Iterables.toArray(feedIDs, FeedID.class)); + } + + public final CompletableFuture> getFollowed(Pagination pagination, FeedID... feeds) throws StreamException { + checkNotNull(feeds, "No feed ids to filter on"); + + final String[] feedIDs = Arrays.stream(feeds) + .map(id -> id.toString()) + .toArray(String[]::new); + final RequestOption[] options = feedIDs.length == 0 + ? new RequestOption[] { pagination } + : new RequestOption[] { pagination, new CustomQueryParameter("filter", String.join(",", feedIDs)) }; + return client + .getFollowed(id, options) + .thenApply(response -> { + try { + return deserializeContainer(response, FollowRelation.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture unfollow(FlatFeed feed) throws StreamException { + return unfollow(feed, io.getstream.core.KeepHistory.NO); + } + + public final CompletableFuture unfollow(FlatFeed feed, io.getstream.core.KeepHistory keepHistory) throws StreamException { + return client + .unfollow(id, feed.getID(), new io.getstream.core.options.KeepHistory(keepHistory)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture updateActivityToTargets(Activity activity, Iterable add, Iterable remove) throws StreamException { + return updateActivityToTargets(activity, Iterables.toArray(add, FeedID.class), Iterables.toArray(remove, FeedID.class)); + } + + public final CompletableFuture updateActivityToTargets(Activity activity, FeedID[] add, FeedID[] remove) throws StreamException { + return client + .updateActivityToTargets(id, activity, add, remove, new FeedID[0]) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture replaceActivityToTargets(Activity activity, Iterable newTargets) throws StreamException { + return replaceActivityToTargets(activity, Iterables.toArray(newTargets, FeedID.class)); + } + + public final CompletableFuture replaceActivityToTargets(Activity activity, FeedID... newTargets) throws StreamException { + return client + .updateActivityToTargets(id, activity, new FeedID[0], new FeedID[0], newTargets) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/client/FileStorageClient.java b/src/main/java/io/getstream/client/FileStorageClient.java new file mode 100644 index 00000000..38836a5a --- /dev/null +++ b/src/main/java/io/getstream/client/FileStorageClient.java @@ -0,0 +1,37 @@ +package io.getstream.client; + +import io.getstream.core.StreamFiles; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.utils.Auth.TokenAction; + +import java.io.File; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import static io.getstream.core.utils.Auth.buildFilesToken; + +public class FileStorageClient { + private final String secret; + private final StreamFiles files; + + FileStorageClient(String secret, StreamFiles files) { + this.secret = secret; + this.files = files; + } + + public CompletableFuture upload(String fileName, byte[] content) throws StreamException { + final Token token = buildFilesToken(secret, TokenAction.WRITE); + return files.upload(token, fileName, content); + } + + public CompletableFuture upload(File content) throws StreamException { + final Token token = buildFilesToken(secret, TokenAction.WRITE); + return files.upload(token, content); + } + + public CompletableFuture delete(URL url) throws StreamException { + final Token token = buildFilesToken(secret, TokenAction.DELETE); + return files.delete(token, url); + } +} diff --git a/src/main/java/io/getstream/client/FlatFeed.java b/src/main/java/io/getstream/client/FlatFeed.java new file mode 100644 index 00000000..3b755168 --- /dev/null +++ b/src/main/java/io/getstream/client/FlatFeed.java @@ -0,0 +1,196 @@ +package io.getstream.client; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import io.getstream.core.models.EnrichedActivity; +import io.getstream.core.models.FeedID; +import io.getstream.core.options.Filter; +import io.getstream.core.options.Pagination; +import io.getstream.core.options.Ranking; +import io.getstream.core.options.RequestOption; +import io.getstream.core.utils.DefaultOptions; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static io.getstream.core.utils.Serialization.deserializeContainer; + +public final class FlatFeed extends Feed { + FlatFeed(Client client, FeedID id) { + super(client, id); + } + + public CompletableFuture> getActivities() throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getActivities(String ranking) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getActivities(Filter filter) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, null); + } + + public CompletableFuture> getActivities(Pagination pagination) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getActivities(Pagination pagination, Filter filter) throws StreamException { + return getActivities(pagination, filter, null); + } + + public CompletableFuture> getActivities(Filter filter, String ranking) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, ranking); + } + + public CompletableFuture> getActivities(Pagination pagination, String ranking) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getActivities(Pagination pagination, Filter filter, String ranking) throws StreamException { + final RequestOption[] options = ranking == null + ? new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER } + : new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER, new Ranking(ranking) }; + return getClient() + .getActivities(getID(), options) + .thenApply(response -> { + try { + return deserializeContainer(response, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture> getCustomActivities(Class type) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getCustomActivities(Class type, String ranking) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getCustomActivities(Class type, Filter filter) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, null); + } + + public CompletableFuture> getCustomActivities(Class type, Pagination pagination) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getCustomActivities(Class type, Pagination pagination, String ranking) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getCustomActivities(type, pagination, filter, null); + } + + public CompletableFuture> getCustomActivities(Class type, Filter filter, String ranking) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, ranking); + } + + public CompletableFuture> getCustomActivities(Class type, Pagination pagination, Filter filter, String ranking) throws StreamException { + final RequestOption[] options = ranking == null + ? new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER } + : new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER, new Ranking(ranking) }; + return getClient() + .getActivities(getID(), options) + .thenApply(response -> { + try { + return deserializeContainer(response, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture> getEnrichedActivities() throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getEnrichedActivities(String ranking) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getEnrichedActivities(Filter filter) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, null); + } + + public CompletableFuture> getEnrichedActivities(Pagination pagination) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getEnrichedActivities(Pagination pagination, Filter filter) throws StreamException { + return getEnrichedActivities(pagination, filter, null); + } + + public CompletableFuture> getEnrichedActivities(Filter filter, String ranking) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, ranking); + } + + public CompletableFuture> getEnrichedActivities(Pagination pagination, String ranking) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getEnrichedActivities(Pagination pagination, Filter filter, String ranking) throws StreamException { + final RequestOption[] options = ranking == null + ? new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER } + : new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER, new Ranking(ranking) }; + return getClient() + .getEnrichedActivities(getID(), options) + .thenApply(response -> { + try { + return deserializeContainer(response, EnrichedActivity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, String ranking) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, null); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Pagination pagination) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Pagination pagination, String ranking) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, pagination, filter, null); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Filter filter, String ranking) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, ranking); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter, String ranking) throws StreamException { + final RequestOption[] options = ranking == null + ? new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER } + : new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER, new Ranking(ranking) }; + return getClient() + .getActivities(getID(), options) + .thenApply(response -> { + try { + return deserializeContainer(response, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/client/ImageStorageClient.java b/src/main/java/io/getstream/client/ImageStorageClient.java new file mode 100644 index 00000000..609dc3bd --- /dev/null +++ b/src/main/java/io/getstream/client/ImageStorageClient.java @@ -0,0 +1,49 @@ +package io.getstream.client; + +import io.getstream.core.StreamImages; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.options.Crop; +import io.getstream.core.options.Resize; +import io.getstream.core.utils.Auth.TokenAction; + +import java.io.File; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import static io.getstream.core.utils.Auth.buildFilesToken; + +public class ImageStorageClient { + private final String secret; + private final StreamImages images; + + ImageStorageClient(String secret, StreamImages images) { + this.secret = secret; + this.images = images; + } + + public CompletableFuture upload(String fileName, byte[] content) throws StreamException { + final Token token = buildFilesToken(secret, TokenAction.WRITE); + return images.upload(token, fileName, content); + } + + public CompletableFuture upload(File content) throws StreamException { + final Token token = buildFilesToken(secret, TokenAction.WRITE); + return images.upload(token, content); + } + + public CompletableFuture delete(URL url) throws StreamException { + final Token token = buildFilesToken(secret, TokenAction.DELETE); + return images.delete(token, url); + } + + public CompletableFuture process(URL url, Crop crop) throws StreamException { + final Token token = buildFilesToken(secret, TokenAction.READ); + return images.process(token, url, crop); + } + + public CompletableFuture process(URL url, Resize resize) throws StreamException { + final Token token = buildFilesToken(secret, TokenAction.READ); + return images.process(token, url, resize); + } +} diff --git a/src/main/java/io/getstream/client/NotificationFeed.java b/src/main/java/io/getstream/client/NotificationFeed.java new file mode 100644 index 00000000..f1d05e0c --- /dev/null +++ b/src/main/java/io/getstream/client/NotificationFeed.java @@ -0,0 +1,216 @@ +package io.getstream.client; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import io.getstream.core.models.EnrichedActivity; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.NotificationGroup; +import io.getstream.core.options.ActivityMarker; +import io.getstream.core.options.Filter; +import io.getstream.core.options.Pagination; +import io.getstream.core.utils.DefaultOptions; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static io.getstream.core.utils.Serialization.deserializeContainer; + +public final class NotificationFeed extends AggregatedFeed { + NotificationFeed(Client client, FeedID id) { + super(client, id); + } + + @Override + public CompletableFuture>> getActivities() throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getActivities(Pagination pagination) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getActivities(Filter filter) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getActivities(ActivityMarker marker) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getActivities(Filter filter, ActivityMarker marker) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + @Override + public CompletableFuture>> getActivities(Pagination pagination, ActivityMarker marker) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getActivities(Pagination pagination, Filter filter) throws StreamException { + return getActivities(pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getActivities(Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, NotificationGroup.class, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Filter filter) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Filter filter, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getCustomActivities(type, pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, NotificationGroup.class, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + @Override + public CompletableFuture>> getEnrichedActivities() throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Pagination pagination) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Filter filter) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedActivities(ActivityMarker marker) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Filter filter, ActivityMarker marker) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Pagination pagination, ActivityMarker marker) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Pagination pagination, Filter filter) throws StreamException { + return getEnrichedActivities(pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getEnrichedActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, NotificationGroup.class, EnrichedActivity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Filter filter, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getEnrichedActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, NotificationGroup.class, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/client/PersonalizationClient.java b/src/main/java/io/getstream/client/PersonalizationClient.java new file mode 100644 index 00000000..e7ed80fe --- /dev/null +++ b/src/main/java/io/getstream/client/PersonalizationClient.java @@ -0,0 +1,73 @@ +package io.getstream.client; + +import com.google.common.collect.ImmutableMap; +import io.getstream.core.StreamPersonalization; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.utils.Auth.TokenAction; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static io.getstream.core.utils.Auth.buildPersonalizationToken; + +public final class PersonalizationClient { + private final String secret; + private final StreamPersonalization personalization; + + PersonalizationClient(String secret, StreamPersonalization personalization) { + this.secret = secret; + this.personalization = personalization; + } + + public CompletableFuture> get(String resource) throws StreamException { + return get(null, resource); + } + + public CompletableFuture> get(String resource, Map params) throws StreamException { + return get(null, resource, params); + } + + public CompletableFuture> get(String userID, String resource) throws StreamException { + return get(userID, resource, ImmutableMap.of()); + } + + public CompletableFuture> get(String userID, String resource, Map params) throws StreamException { + final Token token = buildPersonalizationToken(secret, userID, TokenAction.READ); + return personalization.get(token, userID, resource, params); + } + + public CompletableFuture post(String resource, Map payload) throws StreamException { + return post(null, resource, payload); + } + + public CompletableFuture post(String resource, Map params, Map payload) throws StreamException { + return post(null, resource, params, payload); + } + + public CompletableFuture post(String userID, String resource, Map payload) throws StreamException { + return post(userID, resource, ImmutableMap.of(), payload); + } + + public CompletableFuture post(String userID, String resource, Map params, Map payload) throws StreamException { + final Token token = buildPersonalizationToken(secret, userID, TokenAction.WRITE); + return personalization.post(token, userID, resource, params, payload); + } + + public CompletableFuture delete(String resource) throws StreamException { + return delete(null, resource); + } + + public CompletableFuture delete(String resource, Map params) throws StreamException { + return delete(null, resource, params); + } + + public CompletableFuture delete(String userID, String resource) throws StreamException { + return delete(userID, resource, ImmutableMap.of()); + } + + public CompletableFuture delete(String userID, String resource, Map params) throws StreamException { + final Token token = buildPersonalizationToken(secret, userID, TokenAction.DELETE); + return personalization.delete(token, userID, resource, params); + } +} diff --git a/src/main/java/io/getstream/client/ReactionsClient.java b/src/main/java/io/getstream/client/ReactionsClient.java new file mode 100644 index 00000000..efbd8fb3 --- /dev/null +++ b/src/main/java/io/getstream/client/ReactionsClient.java @@ -0,0 +1,98 @@ +package io.getstream.client; + +import com.google.common.collect.Iterables; +import io.getstream.core.LookupKind; +import io.getstream.core.StreamReactions; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.Reaction; +import io.getstream.core.options.Filter; +import io.getstream.core.utils.Auth.TokenAction; +import io.getstream.core.utils.DefaultOptions; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Auth.buildReactionsToken; + +public final class ReactionsClient { + private final String secret; + private final StreamReactions reactions; + + ReactionsClient(String secret, StreamReactions reactions) { + this.secret = secret; + this.reactions = reactions; + } + + public CompletableFuture get(String id) throws StreamException { + final Token token = buildReactionsToken(secret, TokenAction.READ); + return reactions.get(token, id); + } + + public CompletableFuture> filter(LookupKind lookup, String id) throws StreamException { + return filter(lookup, id, ""); + } + + public CompletableFuture> filter(LookupKind lookup, String id, Filter filter) throws StreamException { + return filter(lookup, id, filter, ""); + } + + public CompletableFuture> filter(LookupKind lookup, String id, String kind) throws StreamException { + return filter(lookup, id, DefaultOptions.DEFAULT_FILTER, kind); + } + + public CompletableFuture> filter(LookupKind lookup, String id, Filter filter, String kind) throws StreamException { + final Token token = buildReactionsToken(secret, TokenAction.READ); + return reactions.filter(token, lookup, id, filter, kind); + } + + public CompletableFuture add(String userID, String kind, String activityID, Iterable targetFeeds) throws StreamException { + return add(userID, kind, activityID, Iterables.toArray(targetFeeds, FeedID.class)); + } + + public CompletableFuture add(String userID, String kind, String activityID, FeedID... targetFeeds) throws StreamException { + checkNotNull(kind, "Reaction kind can't be null"); + checkArgument(!kind.isEmpty(), "Reaction kind can't be empty"); + checkNotNull(activityID, "Reaction activity id can't be null"); + checkArgument(!activityID.isEmpty(), "Reaction activity id can't be empty"); + + return add(userID, Reaction.builder().activityID(activityID).kind(kind).build(), targetFeeds); + } + + public CompletableFuture add(String userID, Reaction reaction, Iterable targetFeeds) throws StreamException { + return add(userID, reaction, Iterables.toArray(targetFeeds, FeedID.class)); + } + + public CompletableFuture add(String userID, Reaction reaction, FeedID... targetFeeds) throws StreamException { + final Token token = buildReactionsToken(secret, TokenAction.WRITE); + return reactions.add(token, userID, reaction, targetFeeds); + } + + public CompletableFuture update(String id, Iterable targetFeeds) throws StreamException { + return update(id, Iterables.toArray(targetFeeds, FeedID.class)); + } + + public CompletableFuture update(String id, FeedID... targetFeeds) throws StreamException { + checkNotNull(id, "Reaction id can't be null"); + checkArgument(!id.isEmpty(), "Reaction id can't be empty"); + + return update(Reaction.builder().id(id).build(), targetFeeds); + } + + public CompletableFuture update(Reaction reaction, Iterable targetFeeds) throws StreamException { + return update(reaction, Iterables.toArray(targetFeeds, FeedID.class)); + } + + public CompletableFuture update(Reaction reaction, FeedID... targetFeeds) throws StreamException { + final Token token = buildReactionsToken(secret, TokenAction.WRITE); + return reactions.update(token, reaction, targetFeeds); + } + + public CompletableFuture delete(String id) throws StreamException { + final Token token = buildReactionsToken(secret, TokenAction.DELETE); + return reactions.delete(token, id); + } +} diff --git a/src/main/java/io/getstream/client/User.java b/src/main/java/io/getstream/client/User.java new file mode 100644 index 00000000..a3da8f8c --- /dev/null +++ b/src/main/java/io/getstream/client/User.java @@ -0,0 +1,98 @@ +package io.getstream.client; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Data; +import io.getstream.core.models.ProfileData; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.deserialize; +import static io.getstream.core.utils.Serialization.deserializeError; + +public class User { + private final Client client; + private final String id; + + public User(Client client, String id) { + checkNotNull(client, "Client can't be null"); + checkNotNull(id, "User ID can't be null"); + checkArgument(!id.isEmpty(), "User ID can't be empty"); + + this.client = client; + this.id = id; + } + + public String getID() { + return id; + } + + public CompletableFuture get() throws StreamException { + return client.getUser(id) + .thenApply(response -> { + try { + return deserialize(response, Data.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture delete() throws StreamException { + return client.deleteUser(id) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture getOrCreate(Data data) throws StreamException { + return client.getOrCreateUser(id, data) + .thenApply(response -> { + try { + return deserialize(response, Data.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture create(Data data) throws StreamException { + return client.createUser(id, data) + .thenApply(response -> { + try { + return deserialize(response, Data.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture update(Data data) throws StreamException { + return client.updateUser(id, data) + .thenApply(response -> { + try { + return deserialize(response, Data.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture profile() throws StreamException { + return client.userProfile(id) + .thenApply(response -> { + try { + return deserialize(response, ProfileData.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudAggregatedFeed.java b/src/main/java/io/getstream/cloud/CloudAggregatedFeed.java new file mode 100644 index 00000000..3a199309 --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudAggregatedFeed.java @@ -0,0 +1,184 @@ +package io.getstream.cloud; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import io.getstream.core.models.EnrichedActivity; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.Group; +import io.getstream.core.options.ActivityMarker; +import io.getstream.core.options.Filter; +import io.getstream.core.options.Pagination; +import io.getstream.core.utils.DefaultOptions; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static io.getstream.core.utils.Serialization.deserializeContainer; + +public class CloudAggregatedFeed extends CloudFeed { + CloudAggregatedFeed(CloudClient client, FeedID id) { + super(client, id); + } + + public CompletableFuture>> getActivities() throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getActivities(Pagination pagination) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getActivities(Filter filter) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getActivities(ActivityMarker marker) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getActivities(Filter filter, ActivityMarker marker) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + public CompletableFuture>> getActivities(Pagination pagination, ActivityMarker marker) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getActivities(Pagination pagination, Filter filter) throws StreamException { + return getActivities(pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getActivities(Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, Group.class, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture>> getCustomActivities(Class type) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getCustomActivities(Class type, Filter filter) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getCustomActivities(Class type, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getCustomActivities(Class type, Filter filter, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getCustomActivities(type, pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, Group.class, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture>> getEnrichedActivities() throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedActivities(Pagination pagination) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedActivities(Filter filter) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedActivities(ActivityMarker marker) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getEnrichedActivities(Filter filter, ActivityMarker marker) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + public CompletableFuture>> getEnrichedActivities(Pagination pagination, ActivityMarker marker) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getEnrichedActivities(Pagination pagination, Filter filter) throws StreamException { + return getEnrichedActivities(pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedActivities(Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getEnrichedActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, Group.class, EnrichedActivity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Filter filter, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getEnrichedActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, Group.class, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudClient.java b/src/main/java/io/getstream/cloud/CloudClient.java new file mode 100644 index 00000000..4b4ca136 --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudClient.java @@ -0,0 +1,259 @@ +package io.getstream.cloud; + +import io.getstream.core.Region; +import io.getstream.core.Stream; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.OKHTTPClientAdapter; +import io.getstream.core.http.Response; +import io.getstream.core.http.Token; +import io.getstream.core.models.Activity; +import io.getstream.core.models.Data; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.OGData; +import io.getstream.core.options.RequestOption; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CloudClient { + private final Token token; + private final String userID; + private final Stream stream; + + private CloudClient(String key, String token, String userID, URL baseURL, HTTPClient httpClient) { + this.token = new Token(token); + this.userID = userID; + this.stream = new Stream(key, baseURL, httpClient); + } + + public static Builder builder(String apiKey, String secret, String userID) { + return new Builder(apiKey, secret, userID); + } + + public CompletableFuture openGraph(URL url) throws StreamException { + return stream.openGraph(token, url); + } + + public static final class Builder { + private static final String DEFAULT_HOST = "stream-io-api.com"; + + private final String apiKey; + private final String token; + private final String userID; + private HTTPClient httpClient; + + private String scheme = "https"; + private String region = Region.US_EAST.toString(); + private String host = DEFAULT_HOST; + private int port = 443; + + public Builder(String apiKey, String token, String userID) { + checkNotNull(apiKey, "API key can't be null"); + checkNotNull(token, "Token can't be null"); + checkNotNull(userID, "User ID can't be null"); + checkArgument(!apiKey.isEmpty(), "API key can't be empty"); + checkArgument(!token.isEmpty(), "Token can't be empty"); + checkArgument(!userID.isEmpty(), "User ID can't be empty"); + this.apiKey = apiKey; + this.token = token; + this.userID = userID; + } + + public Builder httpClient(HTTPClient httpClient) { + checkNotNull(httpClient, "HTTP client can't be null"); + this.httpClient = httpClient; + return this; + } + + public Builder scheme(String scheme) { + checkNotNull(scheme, "Scheme can't be null"); + checkArgument(!scheme.isEmpty(), "Scheme can't be empty"); + this.scheme = scheme; + return this; + } + + public Builder host(String host) { + checkNotNull(host, "Host can't be null"); + checkArgument(!host.isEmpty(), "Host can't be empty"); + this.host = host; + return this; + } + + public Builder port(int port) { + checkArgument(port > 0, "Port has to be a non-zero positive number"); + this.port = port; + return this; + } + + public Builder region(Region region) { + checkNotNull(region, "Region can't be null"); + this.region = region.toString(); + return this; + } + + public Builder region(String region) { + checkNotNull(region, "Region can't be null"); + checkArgument(!region.isEmpty(), "Region can't be empty"); + this.region = region; + return this; + } + + private String buildHost() { + final StringBuilder sb = new StringBuilder(); + if (host.equals(DEFAULT_HOST)) { + sb.append(region).append("."); + } + sb.append(host); + return sb.toString(); + } + + public CloudClient build() throws MalformedURLException { + if (httpClient == null) { + httpClient = new OKHTTPClientAdapter(); + } + return new CloudClient(apiKey, token, userID, new URL(scheme, buildHost(), port, ""), httpClient); + } + } + + public T getHTTPClientImplementation() { + return stream.getHTTPClientImplementation(); + } + + //TODO: add personalized feed versions + public CloudFlatFeed flatFeed(String slug) { + return flatFeed(slug, userID); + } + + public CloudFlatFeed flatFeed(String slug, CloudUser user) { + return flatFeed(slug, user.getID()); + } + + public CloudFlatFeed flatFeed(String slug, String userID) { + return flatFeed(new FeedID(slug, userID)); + } + + public CloudFlatFeed flatFeed(FeedID id) { + return new CloudFlatFeed(this, id); + } + + public CloudAggregatedFeed aggregatedFeed(String slug) { + return aggregatedFeed(slug, userID); + } + + public CloudAggregatedFeed aggregatedFeed(String slug, CloudUser user) { + return aggregatedFeed(slug, user.getID()); + } + + public CloudAggregatedFeed aggregatedFeed(String slug, String userID) { + return aggregatedFeed(new FeedID(slug, userID)); + } + + public CloudAggregatedFeed aggregatedFeed(FeedID id) { + return new CloudAggregatedFeed(this, id); + } + + public CloudNotificationFeed notificationFeed(String slug) { + return notificationFeed(slug, userID); + } + + public CloudNotificationFeed notificationFeed(String slug, CloudUser user) { + return notificationFeed(slug, user.getID()); + } + + public CloudNotificationFeed notificationFeed(String slug, String userID) { + return notificationFeed(new FeedID(slug, userID)); + } + + public CloudNotificationFeed notificationFeed(FeedID id) { + return new CloudNotificationFeed(this, id); + } + + public CloudUser user(String userID) { + return new CloudUser(this, userID); + } + + public CloudCollectionsClient collections() { + return new CloudCollectionsClient(token, userID, stream.collections()); + } + + public CloudReactionsClient reactions() { + return new CloudReactionsClient(token, userID, stream.reactions()); + } + + public CloudFileStorageClient files() { + return new CloudFileStorageClient(token, stream.files()); + } + + public CloudImageStorageClient images() { + return new CloudImageStorageClient(token, stream.images()); + } + + CompletableFuture getActivities(FeedID feed, RequestOption... options) throws StreamException { + return stream.getActivities(token, feed, options); + } + + CompletableFuture getEnrichedActivities(FeedID feed, RequestOption... options) throws StreamException { + return stream.getEnrichedActivities(token, feed, options); + } + + CompletableFuture addActivity(FeedID feed, Activity activity) throws StreamException { + return stream.addActivity(token, feed, activity); + } + + CompletableFuture addActivities(FeedID feed, Activity... activities) throws StreamException { + return stream.addActivities(token, feed, activities); + } + + CompletableFuture removeActivityByID(FeedID feed, String id) throws StreamException { + return stream.removeActivityByID(token, feed, id); + } + + CompletableFuture removeActivityByForeignID(FeedID feed, String foreignID) throws StreamException { + return stream.removeActivityByForeignID(token, feed, foreignID); + } + + CompletableFuture follow(FeedID source, FeedID target, int activityCopyLimit) throws StreamException { + return stream.follow(token, token, source, target, activityCopyLimit); + } + + CompletableFuture getFollowers(FeedID feed, RequestOption... options) throws StreamException { + return stream.getFollowers(token, feed, options); + } + + CompletableFuture getFollowed(FeedID feed, RequestOption... options) throws StreamException { + return stream.getFollowed(token, feed, options); + } + + CompletableFuture unfollow(FeedID source, FeedID target, RequestOption... options) throws StreamException { + return stream.unfollow(token, source, target, options); + } + + CompletableFuture getUser(String id) throws StreamException { + return stream.getUser(token, id, false); + } + + CompletableFuture deleteUser(String id) throws StreamException { + return stream.deleteUser(token, id); + } + + CompletableFuture getOrCreateUser(String id, Data data) throws StreamException { + return stream.createUser(token, id, data, true); + } + + CompletableFuture createUser(String id, Data data) throws StreamException { + return stream.createUser(token, id, data, false); + } + + CompletableFuture updateUser(String id, Data data) throws StreamException { + return stream.updateUser(token, id, data); + } + + CompletableFuture userProfile(String id) throws StreamException { + return stream.getUser(token, id, true); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudCollectionsClient.java b/src/main/java/io/getstream/cloud/CloudCollectionsClient.java new file mode 100644 index 00000000..7d342ee8 --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudCollectionsClient.java @@ -0,0 +1,81 @@ +package io.getstream.cloud; + +import io.getstream.core.StreamCollections; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.models.CollectionData; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import static io.getstream.core.utils.Serialization.convert; + +public class CloudCollectionsClient { + private final Token token; + private final String userID; + private final StreamCollections collections; + + CloudCollectionsClient(Token token, String userID, StreamCollections collections) { + this.token = token; + this.userID = userID; + this.collections = collections; + } + + public CompletableFuture addCustom(String collection, T item) throws StreamException { + return addCustom(userID, collection, item); + } + + public CompletableFuture addCustom(String userID, String collection, T item) throws StreamException { + return add(userID, collection, convert(item, CollectionData.class)) + .thenApply(data -> convert(data, (Class) item.getClass())); + } + + public CompletableFuture add(String collection, CollectionData item) throws StreamException { + return add(userID, collection, item); + } + + public CompletableFuture add(String userID, String collection, CollectionData item) throws StreamException { + return collections.add(token, userID, collection, item); + } + + public CompletableFuture updateCustom(String collection, T item) throws StreamException { + return updateCustom(userID, collection, item); + } + + public CompletableFuture updateCustom(String userID, String collection, T item) throws StreamException { + return update(userID, collection, convert(item, CollectionData.class)) + .thenApply(data -> convert(data, (Class) item.getClass())); + } + + public CompletableFuture update(String collection, CollectionData item) throws StreamException { + return update(userID, collection, item); + } + + public CompletableFuture update(String userID, String collection, CollectionData item) throws StreamException { + return collections.update(token, userID, collection, item); + } + + public CompletableFuture> customItems(Class type, String collection) throws StreamException { + return items(collection) + .thenApply(result -> result.stream() + .map(item -> convert(item, type)) + .collect(Collectors.toList())); + } + + public CompletableFuture> items(String collection) throws StreamException { + return collections.items(token, collection); + } + + public CompletableFuture getCustom(Class type, String collection, String id) throws StreamException { + return get(collection, id).thenApply(data -> convert(data, type)); + } + + public CompletableFuture get(String collection, String id) throws StreamException { + return collections.get(token, collection, id); + } + + public CompletableFuture delete(String collection, String id) throws StreamException { + return collections.delete(token, collection, id); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudFeed.java b/src/main/java/io/getstream/cloud/CloudFeed.java new file mode 100644 index 00000000..47057cdc --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudFeed.java @@ -0,0 +1,246 @@ +package io.getstream.cloud; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Streams; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.FollowRelation; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.Pagination; +import io.getstream.core.options.RequestOption; +import io.getstream.core.utils.DefaultOptions; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.*; + +public class CloudFeed { + private final CloudClient client; + private final FeedID id; + + CloudFeed(CloudClient client, FeedID id) { + checkNotNull(client, "Can't create feed w/o a client"); + checkNotNull(id, "Can't create feed w/o an ID"); + + this.client = client; + this.id = id; + } + + protected final CloudClient getClient() { + return client; + } + + public final FeedID getID() { + return id; + } + + public final String getSlug() { + return id.getSlug(); + } + + public final String getUserID() { + return id.getUserID(); + } + + public final CompletableFuture addActivity(Activity activity) throws StreamException { + return getClient() + .addActivity(id, activity) + .thenApply(response -> { + try { + return deserialize(response, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture addCustomActivity(T activity) throws StreamException { + return getClient() + .addActivity(id, Activity.builder().fromCustomActivity(activity).build()) + .thenApply(response -> { + try { + return deserialize(response, (Class) activity.getClass()); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> addActivities(Iterable activities) throws StreamException { + return addActivities(Iterables.toArray(activities, Activity.class)); + } + + public final CompletableFuture> addCustomActivities(Iterable activities) throws StreamException { + final Activity[] custom = Streams.stream(activities) + .map(activity -> Activity.builder().fromCustomActivity(activity).build()) + .toArray(Activity[]::new); + return getClient() + .addActivities(id, custom) + .thenApply(response -> { + try { + Class element = (Class) ((ParameterizedType) getClass().getGenericSuperclass()) .getActualTypeArguments()[0]; + return deserializeContainer(response, "activities", element); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> addActivities(Activity... activities) throws StreamException { + return getClient() + .addActivities(id, activities) + .thenApply(response -> { + try { + return deserializeContainer(response, "activities", Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> addCustomActivities(T... activities) throws StreamException { + final Activity[] custom = Arrays.stream(activities) + .map(activity -> Activity.builder().fromCustomActivity(activity).build()) + .toArray(Activity[]::new); + return getClient() + .addActivities(id, custom) + .thenApply(response -> { + try { + Class element = (Class) activities.getClass().getComponentType(); + return deserializeContainer(response, "activities", element); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture removeActivityByID(String id) throws StreamException { + return client + .removeActivityByID(this.id, id) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture removeActivityByForeignID(String foreignID) throws StreamException { + return client + .removeActivityByForeignID(id, foreignID) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture follow(CloudFlatFeed feed) throws StreamException { + return follow(feed, DefaultOptions.DEFAULT_ACTIVITY_COPY_LIMIT); + } + + public final CompletableFuture follow(CloudFlatFeed feed, int activityCopyLimit) throws StreamException { + checkArgument(activityCopyLimit <= DefaultOptions.MAX_ACTIVITY_COPY_LIMIT, String.format("Activity copy limit should be less then %d", DefaultOptions.MAX_ACTIVITY_COPY_LIMIT)); + + return client + .follow(id, feed.getID(), activityCopyLimit) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> getFollowers(Iterable feedIDs) throws StreamException { + return getFollowers(DefaultOptions.DEFAULT_PAGINATION, Iterables.toArray(feedIDs, FeedID.class)); + } + + public final CompletableFuture> getFollowers(FeedID... feedIDs) throws StreamException { + return getFollowers(DefaultOptions.DEFAULT_PAGINATION, feedIDs); + } + + public final CompletableFuture> getFollowers(Pagination pagination, Iterable feedIDs) throws StreamException { + return getFollowers(pagination, Iterables.toArray(feedIDs, FeedID.class)); + } + + public final CompletableFuture> getFollowers(Pagination pagination, FeedID... feeds) throws StreamException { + checkNotNull(feeds, "No feed ids to filter on"); + + final String[] feedIDs = Arrays.stream(feeds) + .map(id -> id.toString()) + .toArray(String[]::new); + final RequestOption[] options = feedIDs.length == 0 + ? new RequestOption[] { pagination } + : new RequestOption[] { pagination, new CustomQueryParameter("filter", String.join(",", feedIDs)) }; + return client + .getFollowers(id, options) + .thenApply(response -> { + try { + return deserializeContainer(response, FollowRelation.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture> getFollowed(Iterable feedIDs) throws StreamException { + return getFollowed(DefaultOptions.DEFAULT_PAGINATION, Iterables.toArray(feedIDs, FeedID.class)); + } + + public final CompletableFuture> getFollowed(FeedID... feedIDs) throws StreamException { + return getFollowed(DefaultOptions.DEFAULT_PAGINATION, feedIDs); + } + + public final CompletableFuture> getFollowed(Pagination pagination, Iterable feedIDs) throws StreamException { + return getFollowed(pagination, Iterables.toArray(feedIDs, FeedID.class)); + } + + public final CompletableFuture> getFollowed(Pagination pagination, FeedID... feeds) throws StreamException { + checkNotNull(feeds, "No feed ids to filter on"); + + final String[] feedIDs = Arrays.stream(feeds) + .map(id -> id.toString()) + .toArray(String[]::new); + final RequestOption[] options = feedIDs.length == 0 + ? new RequestOption[] { pagination } + : new RequestOption[] { pagination, new CustomQueryParameter("filter", String.join(",", feedIDs)) }; + return client + .getFollowed(id, options) + .thenApply(response -> { + try { + return deserializeContainer(response, FollowRelation.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public final CompletableFuture unfollow(CloudFlatFeed feed) throws StreamException { + return unfollow(feed, io.getstream.core.KeepHistory.NO); + } + + public final CompletableFuture unfollow(CloudFlatFeed feed, io.getstream.core.KeepHistory keepHistory) throws StreamException { + return client + .unfollow(id, feed.getID(), new io.getstream.core.options.KeepHistory(keepHistory)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudFileStorageClient.java b/src/main/java/io/getstream/cloud/CloudFileStorageClient.java new file mode 100644 index 00000000..c60d77c7 --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudFileStorageClient.java @@ -0,0 +1,31 @@ +package io.getstream.cloud; + +import io.getstream.core.StreamFiles; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; + +import java.io.File; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +public class CloudFileStorageClient { + private final Token token; + private final StreamFiles files; + + CloudFileStorageClient(Token token, StreamFiles files) { + this.token = token; + this.files = files; + } + + public CompletableFuture upload(String fileName, byte[] content) throws StreamException { + return files.upload(token, fileName, content); + } + + public CompletableFuture upload(File content) throws StreamException { + return files.upload(token, content); + } + + public CompletableFuture delete(URL url) throws StreamException { + return files.delete(token, url); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudFlatFeed.java b/src/main/java/io/getstream/cloud/CloudFlatFeed.java new file mode 100644 index 00000000..6111b96b --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudFlatFeed.java @@ -0,0 +1,196 @@ +package io.getstream.cloud; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import io.getstream.core.models.EnrichedActivity; +import io.getstream.core.models.FeedID; +import io.getstream.core.options.Filter; +import io.getstream.core.options.Pagination; +import io.getstream.core.options.Ranking; +import io.getstream.core.options.RequestOption; +import io.getstream.core.utils.DefaultOptions; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static io.getstream.core.utils.Serialization.deserializeContainer; + +public class CloudFlatFeed extends CloudFeed { + CloudFlatFeed(CloudClient client, FeedID id) { + super(client, id); + } + + public CompletableFuture> getActivities() throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getActivities(String ranking) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getActivities(Filter filter) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, null); + } + + public CompletableFuture> getActivities(Pagination pagination) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getActivities(Pagination pagination, Filter filter) throws StreamException { + return getActivities(pagination, filter, null); + } + + public CompletableFuture> getActivities(Filter filter, String ranking) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, ranking); + } + + public CompletableFuture> getActivities(Pagination pagination, String ranking) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getActivities(Pagination pagination, Filter filter, String ranking) throws StreamException { + final RequestOption[] options = ranking == null + ? new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER } + : new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER, new Ranking(ranking) }; + return getClient() + .getActivities(getID(), options) + .thenApply(response -> { + try { + return deserializeContainer(response, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture> getCustomActivities(Class type) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getCustomActivities(Class type, String ranking) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getCustomActivities(Class type, Filter filter) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, null); + } + + public CompletableFuture> getCustomActivities(Class type, Pagination pagination) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getCustomActivities(Class type, Pagination pagination, String ranking) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getCustomActivities(type, pagination, filter, null); + } + + public CompletableFuture> getCustomActivities(Class type, Filter filter, String ranking) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, ranking); + } + + public CompletableFuture> getCustomActivities(Class type, Pagination pagination, Filter filter, String ranking) throws StreamException { + final RequestOption[] options = ranking == null + ? new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER } + : new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER, new Ranking(ranking) }; + return getClient() + .getActivities(getID(), options) + .thenApply(response -> { + try { + return deserializeContainer(response, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture> getEnrichedActivities() throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getEnrichedActivities(String ranking) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getEnrichedActivities(Filter filter) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, null); + } + + public CompletableFuture> getEnrichedActivities(Pagination pagination) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getEnrichedActivities(Pagination pagination, Filter filter) throws StreamException { + return getEnrichedActivities(pagination, filter, null); + } + + public CompletableFuture> getEnrichedActivities(Filter filter, String ranking) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, ranking); + } + + public CompletableFuture> getEnrichedActivities(Pagination pagination, String ranking) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getEnrichedActivities(Pagination pagination, Filter filter, String ranking) throws StreamException { + final RequestOption[] options = ranking == null + ? new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER } + : new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER, new Ranking(ranking) }; + return getClient() + .getEnrichedActivities(getID(), options) + .thenApply(response -> { + try { + return deserializeContainer(response, EnrichedActivity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, String ranking) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, null); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Pagination pagination) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, null); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Pagination pagination, String ranking) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, ranking); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, pagination, filter, null); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Filter filter, String ranking) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, ranking); + } + + public CompletableFuture> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter, String ranking) throws StreamException { + final RequestOption[] options = ranking == null + ? new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER } + : new RequestOption[] { pagination, filter, DefaultOptions.DEFAULT_MARKER, new Ranking(ranking) }; + return getClient() + .getActivities(getID(), options) + .thenApply(response -> { + try { + return deserializeContainer(response, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudImageStorageClient.java b/src/main/java/io/getstream/cloud/CloudImageStorageClient.java new file mode 100644 index 00000000..4d9d0720 --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudImageStorageClient.java @@ -0,0 +1,41 @@ +package io.getstream.cloud; + +import io.getstream.core.StreamImages; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.options.Crop; +import io.getstream.core.options.Resize; + +import java.io.File; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +public class CloudImageStorageClient { + private final Token token; + private final StreamImages images; + + CloudImageStorageClient(Token token, StreamImages images) { + this.token = token; + this.images = images; + } + + public CompletableFuture upload(String fileName, byte[] content) throws StreamException { + return images.upload(token, fileName, content); + } + + public CompletableFuture upload(File content) throws StreamException { + return images.upload(token, content); + } + + public CompletableFuture delete(URL url) throws StreamException { + return images.delete(token, url); + } + + public CompletableFuture process(URL url, Crop crop) throws StreamException { + return images.process(token, url, crop); + } + + public CompletableFuture process(URL url, Resize resize) throws StreamException { + return images.process(token, url, resize); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudNotificationFeed.java b/src/main/java/io/getstream/cloud/CloudNotificationFeed.java new file mode 100644 index 00000000..97bd32c0 --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudNotificationFeed.java @@ -0,0 +1,216 @@ +package io.getstream.cloud; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import io.getstream.core.models.EnrichedActivity; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.NotificationGroup; +import io.getstream.core.options.ActivityMarker; +import io.getstream.core.options.Filter; +import io.getstream.core.options.Pagination; +import io.getstream.core.utils.DefaultOptions; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static io.getstream.core.utils.Serialization.deserializeContainer; + +public class CloudNotificationFeed extends CloudAggregatedFeed { + CloudNotificationFeed(CloudClient client, FeedID id) { + super(client, id); + } + + @Override + public CompletableFuture>> getActivities() throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getActivities(Pagination pagination) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getActivities(Filter filter) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getActivities(ActivityMarker marker) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getActivities(Filter filter, ActivityMarker marker) throws StreamException { + return getActivities(DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + @Override + public CompletableFuture>> getActivities(Pagination pagination, ActivityMarker marker) throws StreamException { + return getActivities(pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getActivities(Pagination pagination, Filter filter) throws StreamException { + return getActivities(pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getActivities(Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, NotificationGroup.class, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Filter filter) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Filter filter, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getCustomActivities(type, pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getCustomActivities(Class type, Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, NotificationGroup.class, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + @Override + public CompletableFuture>> getEnrichedActivities() throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Pagination pagination) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Filter filter) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedActivities(ActivityMarker marker) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Filter filter, ActivityMarker marker) throws StreamException { + return getEnrichedActivities(DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Pagination pagination, ActivityMarker marker) throws StreamException { + return getEnrichedActivities(pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Pagination pagination, Filter filter) throws StreamException { + return getEnrichedActivities(pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedActivities(Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getEnrichedActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, NotificationGroup.class, EnrichedActivity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Filter filter, ActivityMarker marker) throws StreamException { + return getCustomActivities(type, DefaultOptions.DEFAULT_PAGINATION, filter, marker); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, ActivityMarker marker) throws StreamException { + return getEnrichedCustomActivities(type, pagination, DefaultOptions.DEFAULT_FILTER, marker); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter) throws StreamException { + return getEnrichedCustomActivities(type, pagination, filter, DefaultOptions.DEFAULT_MARKER); + } + + @Override + public CompletableFuture>> getEnrichedCustomActivities(Class type, Pagination pagination, Filter filter, ActivityMarker marker) throws StreamException { + return getClient() + .getEnrichedActivities(getID(), pagination, filter, marker) + .thenApply(response -> { + try { + return deserializeContainer(response, NotificationGroup.class, type); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudReactionsClient.java b/src/main/java/io/getstream/cloud/CloudReactionsClient.java new file mode 100644 index 00000000..40605d63 --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudReactionsClient.java @@ -0,0 +1,109 @@ +package io.getstream.cloud; + +import com.google.common.collect.Iterables; +import io.getstream.core.LookupKind; +import io.getstream.core.StreamReactions; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.Token; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.Reaction; +import io.getstream.core.options.Filter; +import io.getstream.core.utils.DefaultOptions; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CloudReactionsClient { + private final Token token; + private final String userID; + private final StreamReactions reactions; + + CloudReactionsClient(Token token, String userID, StreamReactions reactions) { + this.token = token; + this.userID = userID; + this.reactions = reactions; + } + + public CompletableFuture get(String id) throws StreamException { + return reactions.get(token, id); + } + + public CompletableFuture> filter(LookupKind lookup, String id) throws StreamException { + return filter(lookup, id, ""); + } + + public CompletableFuture> filter(LookupKind lookup, String id, Filter filter) throws StreamException { + return filter(lookup, id, filter, ""); + } + + public CompletableFuture> filter(LookupKind lookup, String id, String kind) throws StreamException { + return filter(lookup, id, DefaultOptions.DEFAULT_FILTER, kind); + } + + public CompletableFuture> filter(LookupKind lookup, String id, Filter filter, String kind) throws StreamException { + return reactions.filter(token, lookup, id, filter, kind); + } + + public CompletableFuture add(String kind, String activityID, Iterable targetFeeds) throws StreamException { + return add(userID, kind, activityID, targetFeeds); + } + + public CompletableFuture add(String kind, String activityID, FeedID... targetFeeds) throws StreamException { + return add(userID, activityID, targetFeeds); + } + + public CompletableFuture add(Reaction reaction, Iterable targetFeeds) throws StreamException { + return add(userID, reaction, targetFeeds); + } + + public CompletableFuture add(Reaction reaction, FeedID... targetFeeds) throws StreamException { + return add(userID, reaction, targetFeeds); + } + + public CompletableFuture add(String userID, String kind, String activityID, Iterable targetFeeds) throws StreamException { + return add(userID, kind, activityID, Iterables.toArray(targetFeeds, FeedID.class)); + } + + public CompletableFuture add(String userID, String kind, String activityID, FeedID... targetFeeds) throws StreamException { + checkNotNull(kind, "Reaction kind can't be null"); + checkArgument(!kind.isEmpty(), "Reaction kind can't be empty"); + checkNotNull(activityID, "Reaction activity id can't be null"); + checkArgument(!activityID.isEmpty(), "Reaction activity id can't be empty"); + + return add(userID, Reaction.builder().activityID(activityID).kind(kind).build(), targetFeeds); + } + + public CompletableFuture add(String userID, Reaction reaction, Iterable targetFeeds) throws StreamException { + return add(userID, reaction, Iterables.toArray(targetFeeds, FeedID.class)); + } + + public CompletableFuture add(String userID, Reaction reaction, FeedID... targetFeeds) throws StreamException { + return reactions.add(token, userID, reaction, targetFeeds); + } + + public CompletableFuture update(String id, Iterable targetFeeds) throws StreamException { + return update(id, Iterables.toArray(targetFeeds, FeedID.class)); + } + + public CompletableFuture update(String id, FeedID... targetFeeds) throws StreamException { + checkNotNull(id, "Reaction id can't be null"); + checkArgument(!id.isEmpty(), "Reaction id can't be empty"); + + return update(Reaction.builder().id(id).build(), targetFeeds); + } + + public CompletableFuture update(Reaction reaction, Iterable targetFeeds) throws StreamException { + return update(reaction, Iterables.toArray(targetFeeds, FeedID.class)); + } + + public CompletableFuture update(Reaction reaction, FeedID... targetFeeds) throws StreamException { + return reactions.update(token, reaction, targetFeeds); + } + + public CompletableFuture delete(String id) throws StreamException { + return reactions.delete(token, id); + } +} diff --git a/src/main/java/io/getstream/cloud/CloudUser.java b/src/main/java/io/getstream/cloud/CloudUser.java new file mode 100644 index 00000000..17b0a1df --- /dev/null +++ b/src/main/java/io/getstream/cloud/CloudUser.java @@ -0,0 +1,98 @@ +package io.getstream.cloud; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Data; +import io.getstream.core.models.ProfileData; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.deserialize; +import static io.getstream.core.utils.Serialization.deserializeError; + +public class CloudUser { + private final CloudClient client; + private final String id; + + public CloudUser(CloudClient client, String id) { + checkNotNull(client, "Client can't be null"); + checkNotNull(id, "User ID can't be null"); + checkArgument(!id.isEmpty(), "User ID can't be empty"); + + this.client = client; + this.id = id; + } + + public String getID() { + return id; + } + + public CompletableFuture get() throws StreamException { + return client.getUser(id) + .thenApply(response -> { + try { + return deserialize(response, Data.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture delete() throws StreamException { + return client.deleteUser(id) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture getOrCreate(Data data) throws StreamException { + return client.getOrCreateUser(id, data) + .thenApply(response -> { + try { + return deserialize(response, Data.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture create(Data data) throws StreamException { + return client.createUser(id, data) + .thenApply(response -> { + try { + return deserialize(response, Data.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture update(Data data) throws StreamException { + return client.updateUser(id, data) + .thenApply(response -> { + try { + return deserialize(response, Data.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } + + public CompletableFuture profile() throws StreamException { + return client.userProfile(id) + .thenApply(response -> { + try { + return deserialize(response, ProfileData.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } +} diff --git a/src/main/java/io/getstream/core/KeepHistory.java b/src/main/java/io/getstream/core/KeepHistory.java new file mode 100644 index 00000000..92ff2a86 --- /dev/null +++ b/src/main/java/io/getstream/core/KeepHistory.java @@ -0,0 +1,16 @@ +package io.getstream.core; + +public enum KeepHistory { + YES(true), + NO(false); + + private final boolean flag; + + KeepHistory(boolean flag) { + this.flag = flag; + } + + public boolean getFlag() { + return flag; + } +} diff --git a/src/main/java/io/getstream/core/LookupKind.java b/src/main/java/io/getstream/core/LookupKind.java new file mode 100644 index 00000000..0c4b6a78 --- /dev/null +++ b/src/main/java/io/getstream/core/LookupKind.java @@ -0,0 +1,16 @@ +package io.getstream.core; + +public enum LookupKind { + ACTIVITY("activity_id"), + USER("user_id"); + + private final String kind; + + LookupKind(String kind) { + this.kind = kind; + } + + public String getKind() { + return kind; + } +} diff --git a/src/main/java/io/getstream/core/Region.java b/src/main/java/io/getstream/core/Region.java new file mode 100644 index 00000000..ce393e6a --- /dev/null +++ b/src/main/java/io/getstream/core/Region.java @@ -0,0 +1,20 @@ +package io.getstream.core; + +public enum Region { + US_EAST("us-east-api"), + TOKYO("tokyo-api"), + DUBLIN("dublin-api"), + SINGAPORE("singapore-api"), + CANADA("ca-central-1"); + + private final String region; + + Region(String region) { + this.region = region; + } + + @Override + public String toString() { + return region; + } +} diff --git a/src/main/java/io/getstream/core/Stream.java b/src/main/java/io/getstream/core/Stream.java new file mode 100644 index 00000000..9639fa72 --- /dev/null +++ b/src/main/java/io/getstream/core/Stream.java @@ -0,0 +1,369 @@ +package io.getstream.core; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.core.JsonProcessingException; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Response; +import io.getstream.core.http.Token; +import io.getstream.core.models.Activity; +import io.getstream.core.models.Data; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.OGData; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.RequestOption; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Request.*; +import static io.getstream.core.utils.Routes.*; +import static io.getstream.core.utils.Serialization.deserialize; +import static io.getstream.core.utils.Serialization.toJSON; + +public final class Stream { + private final String key; + private final URL baseURL; + private final HTTPClient httpClient; + + public Stream(String key, URL baseURL, HTTPClient httpClient) { + this.key = key; + this.baseURL = baseURL; + this.httpClient = httpClient; + } + + public StreamBatch batch() { + return new StreamBatch(key, baseURL, httpClient); + } + + public StreamCollections collections() { + return new StreamCollections(key, baseURL, httpClient); + } + + public StreamPersonalization personalization() { + return new StreamPersonalization(key, baseURL, httpClient); + } + + public StreamAnalytics analytics() { + return new StreamAnalytics(key, baseURL, httpClient); + } + + public StreamReactions reactions() { + return new StreamReactions(key, baseURL, httpClient); + } + + public StreamFiles files() { + return new StreamFiles(key, baseURL, httpClient); + } + + public StreamImages images() { + return new StreamImages(key, baseURL, httpClient); + } + + public CompletableFuture updateActivityByID(Token token, String id, Map set, String[] unset) throws StreamException { + checkNotNull(id, "No activity to update"); + checkNotNull(set, "No activity properties to set"); + checkNotNull(unset, "No activity properties to unset"); + + try { + //XXX: renaming variables so we can unambiguously name payload fields 'id', 'set', 'unset' + String activityID = id; + Map propertiesToSet = set; + String[] propertiesToUnset = unset; + final byte[] payload = toJSON(new Object() { + public final String id = activityID; + public final Map set = propertiesToSet; + public final String[] unset = propertiesToUnset; + }); + final URL url = new URL(baseURL, "activity/"); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserialize(response, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture updateActivityByForeignID(Token token, String foreignID, Date timestamp, Map set, String[] unset) throws StreamException { + checkNotNull(foreignID, "No activity to update"); + checkNotNull(timestamp, "Missing timestamp"); + checkNotNull(set, "No activity properties to set"); + checkNotNull(unset, "No activity properties to unset"); + + try { + //XXX: renaming variables so we can unambiguously name payload fields 'set', 'unset' + Map propertiesToSet = set; + String[] propertiesToUnset = unset; + final byte[] payload = toJSON(new Object() { + public final String foreign_id = foreignID; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + public final Date time = timestamp; + public final Map set = propertiesToSet; + public final String[] unset = propertiesToUnset; + }); + final URL url = new URL(baseURL, "activity/"); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserialize(response, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture openGraph(Token token, URL targetURL) throws StreamException { + checkNotNull(targetURL, "Missing url"); + + try { + final URL url = buildOpenGraphURL(baseURL); + return httpClient.execute(buildGet(url, key, token, new CustomQueryParameter("url", targetURL.toExternalForm()))) + .thenApply(response -> { + try { + return deserialize(response, OGData.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public T getHTTPClientImplementation() { + return httpClient.getImplementation(); + } + + public CompletableFuture getActivities(Token token, FeedID feed, RequestOption... options) throws StreamException { + checkNotNull(options, "Missing request options"); + + try { + final URL url = buildFeedURL(baseURL, feed, "/"); + return httpClient.execute(buildGet(url, key, token, options)); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture getEnrichedActivities(Token token, FeedID feed, RequestOption... options) throws StreamException { + checkNotNull(options, "Missing request options"); + + try { + final URL url = buildEnrichedFeedURL(baseURL, feed, "/"); + return httpClient.execute(buildGet(url, key, token, options)); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture addActivity(Token token, FeedID feed, Activity activity) throws StreamException { + checkNotNull(activity, "No activity to add"); + + try { + final byte[] payload = toJSON(activity); + final URL url = buildFeedURL(baseURL, feed, "/"); + return httpClient.execute(buildPost(url, key, token, payload)); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture addActivities(Token token, FeedID feed, Activity... activityObjects) throws StreamException { + checkNotNull(activityObjects, "No activities to add"); + + try { + final byte[] payload = toJSON(new Object() { + public final Activity[] activities = activityObjects; + }); + final URL url = buildFeedURL(baseURL, feed, "/"); + return httpClient.execute(buildPost(url, key, token, payload)); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture removeActivityByID(Token token, FeedID feed, String id) throws StreamException { + checkNotNull(id, "No activity id to remove"); + + try { + final URL url = buildFeedURL(baseURL, feed, '/' + id + '/'); + return httpClient.execute(buildDelete(url, key, token)); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture removeActivityByForeignID(Token token, FeedID feed, String foreignID) throws StreamException { + checkNotNull(foreignID, "No activity id to remove"); + + try { + final URL url = buildFeedURL(baseURL, feed, '/' + foreignID + '/'); + return httpClient.execute(buildDelete(url, key, token, new CustomQueryParameter("foreign_id", "1"))); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture follow(Token token, Token targetToken, FeedID sourceFeed, FeedID targetFeed, int activityCopyLimit) throws StreamException { + checkNotNull(targetFeed, "No feed to follow"); + checkArgument(sourceFeed != targetFeed, "Feed can't follow itself"); + checkArgument(activityCopyLimit >= 0, "Activity copy limit should be a non-negative number"); + + try { + final byte[] payload = toJSON(new Object() { + public String target = targetFeed.toString(); + public int activity_copy_limit = activityCopyLimit; + public String target_token = targetToken.toString(); + }); + final URL url = buildFeedURL(baseURL, sourceFeed, "/following/"); + return httpClient.execute(buildPost(url, key, token, payload)); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture getFollowers(Token token, FeedID feed, RequestOption... options) throws StreamException { + checkNotNull(options, "Missing request options"); + + try { + final URL url = buildFeedURL(baseURL, feed, "/followers/"); + return httpClient.execute(buildGet(url, key, token, options)); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture getFollowed(Token token, FeedID feed, RequestOption... options) throws StreamException { + checkNotNull(options, "Missing request options"); + + try { + final URL url = buildFeedURL(baseURL, feed, "/following/"); + return httpClient.execute(buildGet(url, key, token, options)); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture unfollow(Token token, FeedID source, FeedID target, RequestOption... options) throws StreamException { + checkNotNull(options, "Missing request options"); + checkNotNull(target, "No target feed to unfollow"); + + try { + final URL url = buildFeedURL(baseURL, source, "/following/" + target + '/'); + return httpClient.execute(buildDelete(url, key, token, options)); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture updateActivityToTargets(Token token, FeedID feed, Activity activity, FeedID[] add, FeedID[] remove, FeedID[] replace) throws StreamException { + checkNotNull(activity, "No activity to update"); + checkNotNull(activity.getForeignID(), "Activity is required to have foreign ID attribute"); + checkNotNull(activity.getTime(), "Activity is required to have time attribute"); + checkNotNull(add, "No targets to add"); + checkNotNull(remove, "No targets to remove"); + checkNotNull(replace, "No targets to set"); + boolean modification = replace.length == 0 && (add.length > 0 || remove.length > 0); + boolean replacement = replace.length > 0 && add.length == 0 && remove.length == 0; + checkArgument(modification || replacement, "Can't replace and modify activity to targets at the same time"); + + final String[] addedTargets = Arrays.stream(add) + .map(id -> id.toString()) + .toArray(String[]::new); + final String[] removedTargets = Arrays.stream(remove) + .map(id -> id.toString()) + .toArray(String[]::new); + final String[] newTargets = Arrays.stream(replace) + .map(id -> id.toString()) + .toArray(String[]::new); + + try { + final byte[] payload = toJSON(new Object() { + public String foreign_id = activity.getForeignID(); + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + public Date time = activity.getTime(); + public String[] added_targets = addedTargets; + public String[] removed_targets = removedTargets; + public String[] new_targets = newTargets; + }); + final URL url = buildToTargetUpdateURL(baseURL, feed); + return httpClient.execute(buildPost(url, key, token, payload)); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture getUser(Token token, String id, boolean withFollowCounts) throws StreamException { + checkNotNull(id, "Missing user ID"); + checkArgument(!id.isEmpty(), "Missing user ID"); + + try { + final URL url = buildUsersURL(baseURL, id + '/'); + return httpClient.execute(buildGet(url, key, token, new CustomQueryParameter("with_follow_counts", Boolean.toString(withFollowCounts)))); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture deleteUser(Token token, String id) throws StreamException { + checkNotNull(id, "Missing user ID"); + checkArgument(!id.isEmpty(), "Missing user ID"); + + try { + final URL url = buildUsersURL(baseURL, id + '/'); + return httpClient.execute(buildDelete(url, key, token)); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture createUser(Token token, String userID, Data userData, boolean getOrCreate) throws StreamException { + checkNotNull(userID, "Missing user ID"); + checkNotNull(userData, "Missing user data"); + checkArgument(!userID.isEmpty(), "Missing user ID"); + + try { + final byte[] payload = toJSON(new Object() { + public String id = userID; + public Map data = userData.getData(); + }); + final URL url = buildUsersURL(baseURL); + return httpClient.execute(buildPost(url, key, token, payload, new CustomQueryParameter("get_or_create", Boolean.toString(getOrCreate)))); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture updateUser(Token token, String userID, Data userData) throws StreamException { + checkNotNull(userID, "Missing user ID"); + checkNotNull(userData, "Missing user data"); + checkArgument(!userID.isEmpty(), "Missing user ID"); + + try { + final byte[] payload = toJSON(new Object() { + public Map data = userData.getData(); + }); + final URL url = buildUsersURL(baseURL, userID + '/'); + return httpClient.execute(buildPut(url, key, token, payload)); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } +} diff --git a/src/main/java/io/getstream/core/StreamAnalytics.java b/src/main/java/io/getstream/core/StreamAnalytics.java new file mode 100644 index 00000000..f45b1a98 --- /dev/null +++ b/src/main/java/io/getstream/core/StreamAnalytics.java @@ -0,0 +1,93 @@ +package io.getstream.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.base.Charsets; +import com.google.common.collect.ObjectArrays; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Token; +import io.getstream.core.models.Engagement; +import io.getstream.core.models.Impression; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Request.buildPost; +import static io.getstream.core.utils.Routes.buildAnalyticsURL; +import static io.getstream.core.utils.Serialization.deserializeError; +import static io.getstream.core.utils.Serialization.toJSON; + +public final class StreamAnalytics { + private final String key; + private final URL baseURL; + private final HTTPClient httpClient; + + StreamAnalytics(String key, URL baseURL, HTTPClient httpClient) { + this.key = key; + this.baseURL = baseURL; + this.httpClient = httpClient; + } + + public CompletableFuture trackEngagement(Token token, Engagement... events) throws StreamException { + checkNotNull(events, "No events to track"); + checkArgument(events.length > 0, "No events to track"); + + try { + final byte[] payload = toJSON(new Object() { + public final Engagement[] content_list = events; + }); + final URL url = buildAnalyticsURL(baseURL, "engagement/"); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture trackImpression(Token token, Impression event) throws StreamException { + checkNotNull(event, "No events to track"); + + try { + final byte[] payload = toJSON(event); + final URL url = buildAnalyticsURL(baseURL, "impression/"); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public URL createRedirectURL(Token token, URL url, Impression[] impressions, Engagement[] engagements) throws StreamException { + try { + final byte[] events = toJSON(ObjectArrays.concat(impressions, engagements, Object.class)); + return HTTPClient.requestBuilder() + .url(buildAnalyticsURL(baseURL, "redirect/")) + .addQueryParameter("api_key", key) + .addQueryParameter("url", url.toExternalForm()) + .addQueryParameter("events", new String(events, Charsets.UTF_8)) + .addQueryParameter("auth_type", "jwt") + .addQueryParameter("authorization", token.toString()) + .build().getURL(); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } +} diff --git a/src/main/java/io/getstream/core/StreamBatch.java b/src/main/java/io/getstream/core/StreamBatch.java new file mode 100644 index 00000000..5dd3adca --- /dev/null +++ b/src/main/java/io/getstream/core/StreamBatch.java @@ -0,0 +1,183 @@ +package io.getstream.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Token; +import io.getstream.core.models.Activity; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.FollowRelation; +import io.getstream.core.models.ForeignIDTimePair; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.RequestOption; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Request.buildGet; +import static io.getstream.core.utils.Request.buildPost; +import static io.getstream.core.utils.Routes.buildActivitiesURL; +import static io.getstream.core.utils.Serialization.*; + +public final class StreamBatch { + private static final SimpleDateFormat timestampFormat = new SimpleDateFormat("yyyy-MM-dd"); + + private final String key; + private final URL baseURL; + private final HTTPClient httpClient; + + public StreamBatch(String key, URL baseURL, HTTPClient httpClient) { + this.key = key; + this.baseURL = baseURL; + this.httpClient = httpClient; + } + + public CompletableFuture addToMany(Token token, Activity activity, FeedID... feeds) throws StreamException { + checkNotNull(activity, "Missing activity"); + checkNotNull(feeds, "No feeds to add to"); + checkArgument(feeds.length > 0, "No feeds to add to"); + + //XXX: renaming the variable so we can unambiguously name payload field 'activity' + Activity data = activity; + String[] feedIDs = Arrays.stream(feeds).map(feed -> feed.toString()).toArray(String[]::new); + try { + final byte[] payload = toJSON(new Object() { + public final Activity activity = data; + public final String[] feed_ids = feedIDs; + }); + final URL url = new URL(baseURL, "feed/add_to_many/"); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture followMany(Token token, int activityCopyLimit, FollowRelation... follows) throws StreamException { + checkArgument(activityCopyLimit >= 0, "Activity copy limit must be non negative"); + checkNotNull(follows, "No feeds to follow"); + checkArgument(follows.length > 0, "No feeds to follow"); + + try { + final byte[] payload = toJSON(follows); + final URL url = new URL(baseURL, "follow_many/"); + return httpClient.execute(buildPost(url, key, token, payload, new CustomQueryParameter("activity_copy_limit", Integer.toString(activityCopyLimit)))) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture unfollowMany(Token token, FollowRelation... follows) throws StreamException { + checkNotNull(follows, "No feeds to unfollow"); + checkArgument(follows.length > 0, "No feeds to unfollow"); + + try { + final byte[] payload = toJSON(follows); + final URL url = new URL(baseURL, "unfollow_many/"); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture> getActivitiesByID(Token token, String... activityIDs) throws StreamException { + checkNotNull(activityIDs, "No activities to update"); + checkArgument(activityIDs.length > 0, "No activities to update"); + + try { + final URL url = buildActivitiesURL(baseURL); + return httpClient.execute(buildGet(url, key, token, new CustomQueryParameter("ids", String.join(",", activityIDs)))) + .thenApply(response -> { + try { + return deserializeContainer(response, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture> getActivitiesByForeignID(Token token, ForeignIDTimePair... activityIDTimePairs) throws StreamException { + checkNotNull(activityIDTimePairs, "No activities to get"); + checkArgument(activityIDTimePairs.length > 0, "No activities to get"); + + String[] foreignIDs = Arrays.stream(activityIDTimePairs) + .map(pair -> pair.getForeignID()) + .toArray(String[]::new); + String[] timestamps = Arrays.stream(activityIDTimePairs) + .map(pair -> timestampFormat.format(pair.getTime())) + .toArray(String[]::new); + try { + final URL url = buildActivitiesURL(baseURL); + final RequestOption[] options = new RequestOption[]{ + new CustomQueryParameter("foreign_ids", String.join(",", foreignIDs)), + new CustomQueryParameter("timestamps", String.join(",", timestamps)) + }; + return httpClient.execute(buildGet(url, key, token, options)) + .thenApply(response -> { + try { + return deserializeContainer(response, Activity.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture updateActivities(Token token, Activity... activities) throws StreamException { + checkNotNull(activities, "No activities to update"); + checkArgument(activities.length > 0, "No activities to update"); + + try { + //XXX: renaming the variable so we can unambiguously name payload field 'activities' + Activity[] data = activities; + final byte[] payload = toJSON(new Object() { + public final Activity[] activities = data; + }); + final URL url = buildActivitiesURL(baseURL); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } +} diff --git a/src/main/java/io/getstream/core/StreamCollections.java b/src/main/java/io/getstream/core/StreamCollections.java new file mode 100644 index 00000000..7281d531 --- /dev/null +++ b/src/main/java/io/getstream/core/StreamCollections.java @@ -0,0 +1,220 @@ +package io.getstream.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableMap; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Token; +import io.getstream.core.models.CollectionData; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.RequestOption; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Request.*; +import static io.getstream.core.utils.Routes.buildBatchCollectionsURL; +import static io.getstream.core.utils.Routes.buildCollectionsURL; +import static io.getstream.core.utils.Serialization.*; + +public final class StreamCollections { + private final String key; + private final URL baseURL; + private final HTTPClient httpClient; + + StreamCollections(String key, URL baseURL, HTTPClient httpClient) { + this.key = key; + this.baseURL = baseURL; + this.httpClient = httpClient; + } + + public CompletableFuture add(Token token, String userID, String collection, CollectionData item) throws StreamException { + checkNotNull(collection, "Collection name can't be null"); + checkArgument(!collection.isEmpty(), "Collection name can't be empty"); + checkNotNull(item, "Collection data can't be null"); + + try { + final byte[] payload = toJSON(new Object() { + public final String user_id = firstNonNull(userID, ""); + public final Map data = item.getData(); + }); + final URL url = buildCollectionsURL(baseURL, collection + '/'); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserialize(response, CollectionData.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture update(Token token, String userID, String collection, CollectionData item) throws StreamException { + checkNotNull(collection, "Collection name can't be null"); + checkArgument(!collection.isEmpty(), "Collection name can't be empty"); + checkNotNull(item, "Collection data can't be null"); + + try { + final byte[] payload = toJSON(new Object() { + public final String user_id = firstNonNull(userID, ""); + public final Map data = item.getData(); + }); + final URL url = buildCollectionsURL(baseURL, collection + '/' + item.getID() + '/'); + return httpClient.execute(buildPut(url, key, token, payload)) + .thenApply(response -> { + try { + return deserialize(response, CollectionData.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture upsertMany(Token token, String collection, CollectionData... items) throws StreamException { + checkNotNull(collection, "Collection name can't be null"); + checkArgument(!collection.isEmpty(), "Collection name can't be empty"); + checkArgument(items.length > 0, "Collection data can't be empty"); + + try { + final byte[] payload = toJSON(new Object() { + public final Map data = ImmutableMap.of(collection, items); + }); + final URL url = buildBatchCollectionsURL(baseURL); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture> items(Token token, String collection) throws StreamException { + checkNotNull(collection, "Collection name can't be null"); + checkArgument(!collection.isEmpty(), "Collection name can't be empty"); + + try { + final URL url = buildCollectionsURL(baseURL, collection + '/'); + return httpClient.execute(buildGet(url, key, token)) + .thenApply(response -> { + try { + return deserializeContainer(response, CollectionData.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture get(Token token, String collection, String id) throws StreamException { + checkNotNull(collection, "Collection name can't be null"); + checkArgument(!collection.isEmpty(), "Collection name can't be empty"); + checkNotNull(id, "Collection id can't be null"); + checkArgument(!id.isEmpty(), "Collection id can't be empty"); + + try { + final URL url = buildCollectionsURL(baseURL, collection + '/' + id + '/'); + return httpClient.execute(buildGet(url, key, token)) + .thenApply(response -> { + try { + return deserialize(response, CollectionData.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture> getMany(Token token, String collection, String... ids) throws StreamException { + checkNotNull(collection, "Collection name can't be null"); + checkArgument(!collection.isEmpty(), "Collection name can't be empty"); + checkArgument(ids.length > 0, "Collection ids can't be empty"); + + List foreignIDs = Arrays.stream(ids) + .map(id -> String.format("%s:%s", collection, id)) + .collect(Collectors.toList()); + try { + final URL url = buildBatchCollectionsURL(baseURL); + return httpClient.execute(buildGet(url, key, token, new CustomQueryParameter("foreign_ids", String.join(",", foreignIDs)))) + .thenApply(response -> { + try { + return deserializeContainer(response, "response.data", CollectionData.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture delete(Token token, String collection, String id) throws StreamException { + checkNotNull(collection, "Collection name can't be null"); + checkArgument(!collection.isEmpty(), "Collection name can't be empty"); + checkNotNull(id, "Collection id can't be null"); + checkArgument(!id.isEmpty(), "Collection id can't be empty"); + + try { + final URL url = buildCollectionsURL(baseURL, collection + '/' + id + '/'); + return httpClient.execute(buildDelete(url, key, token)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture deleteMany(Token token, String collection, String... ids) throws StreamException { + checkNotNull(collection, "Collection name can't be null"); + checkArgument(!collection.isEmpty(), "Collection name can't be empty"); + checkArgument(ids.length > 0, "Collection ids can't be empty"); + + try { + final URL url = buildBatchCollectionsURL(baseURL); + final RequestOption[] options = new RequestOption[] { + new CustomQueryParameter("collection_name", collection), + new CustomQueryParameter("ids", String.join(",", ids)) + }; + return httpClient.execute(buildDelete(url, key, token, options)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } +} diff --git a/src/main/java/io/getstream/core/StreamFiles.java b/src/main/java/io/getstream/core/StreamFiles.java new file mode 100644 index 00000000..cf35d2a6 --- /dev/null +++ b/src/main/java/io/getstream/core/StreamFiles.java @@ -0,0 +1,90 @@ +package io.getstream.core; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Token; +import io.getstream.core.options.CustomQueryParameter; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Request.buildDelete; +import static io.getstream.core.utils.Request.buildMultiPartPost; +import static io.getstream.core.utils.Routes.buildFilesURL; +import static io.getstream.core.utils.Serialization.deserialize; +import static io.getstream.core.utils.Serialization.deserializeError; + +public class StreamFiles { + private final String key; + private final URL baseURL; + private final HTTPClient httpClient; + + StreamFiles(String key, URL baseURL, HTTPClient httpClient) { + this.key = key; + this.baseURL = baseURL; + this.httpClient = httpClient; + } + + public CompletableFuture upload(Token token, String fileName, byte[] content) throws StreamException { + checkNotNull(content, "No data to upload"); + checkArgument(content.length > 0, "No data to upload"); + + try { + final URL url = buildFilesURL(baseURL); + return httpClient.execute(buildMultiPartPost(url, key, token, fileName, content)) + .thenApply(response -> { + try { + return deserialize(response, "file", URL.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture upload(Token token, File content) throws StreamException { + checkNotNull(content, "No file to upload"); + checkArgument(content.exists(), "No file to upload"); + + try { + final URL url = buildFilesURL(baseURL); + return httpClient.execute(buildMultiPartPost(url, key, token, content)) + .thenApply(response -> { + try { + return deserialize(response, "file", URL.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture delete(Token token, URL targetURL) throws StreamException { + checkNotNull(targetURL, "No file to delete"); + + try { + final URL url = buildFilesURL(baseURL); + return httpClient.execute(buildDelete(url, key, token, new CustomQueryParameter("url", targetURL.toExternalForm()))) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } +} diff --git a/src/main/java/io/getstream/core/StreamImages.java b/src/main/java/io/getstream/core/StreamImages.java new file mode 100644 index 00000000..5775ba2a --- /dev/null +++ b/src/main/java/io/getstream/core/StreamImages.java @@ -0,0 +1,110 @@ +package io.getstream.core; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Token; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.RequestOption; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Request.buildDelete; +import static io.getstream.core.utils.Request.buildGet; +import static io.getstream.core.utils.Request.buildMultiPartPost; +import static io.getstream.core.utils.Routes.buildImagesURL; +import static io.getstream.core.utils.Serialization.deserialize; +import static io.getstream.core.utils.Serialization.deserializeError; + +public class StreamImages { + private final String key; + private final URL baseURL; + private final HTTPClient httpClient; + + StreamImages(String key, URL baseURL, HTTPClient httpClient) { + this.key = key; + this.baseURL = baseURL; + this.httpClient = httpClient; + } + + public CompletableFuture upload(Token token, String fileName, byte[] content) throws StreamException { + checkNotNull(content, "No data to upload"); + checkArgument(content.length > 0, "No data to upload"); + + try { + final URL url = buildImagesURL(baseURL); + return httpClient.execute(buildMultiPartPost(url, key, token, fileName, content)) + .thenApply(response -> { + try { + return deserialize(response, "file", URL.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture upload(Token token, File content) throws StreamException { + checkNotNull(content, "No file to upload"); + checkArgument(content.exists(), "No file to upload"); + + try { + final URL url = buildImagesURL(baseURL); + return httpClient.execute(buildMultiPartPost(url, key, token, content)) + .thenApply(response -> { + try { + return deserialize(response, "file", URL.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture delete(Token token, URL targetURL) throws StreamException { + checkNotNull(targetURL, "No image to delete"); + + try { + final URL url = buildImagesURL(baseURL); + return httpClient.execute(buildDelete(url, key, token, new CustomQueryParameter("url", targetURL.toExternalForm()))) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture process(Token token, URL targetURL, RequestOption option) throws StreamException { + checkNotNull(targetURL, "No image to process"); + + try { + final URL url = buildImagesURL(baseURL); + return httpClient.execute(buildGet(url, key, token, option, new CustomQueryParameter("url", targetURL.toExternalForm()))) + .thenApply(response -> { + try { + return deserialize(response, "file", URL.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } +} diff --git a/src/main/java/io/getstream/core/StreamPersonalization.java b/src/main/java/io/getstream/core/StreamPersonalization.java new file mode 100644 index 00000000..929d24b7 --- /dev/null +++ b/src/main/java/io/getstream/core/StreamPersonalization.java @@ -0,0 +1,108 @@ +package io.getstream.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Token; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.RequestOption; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Request.*; +import static io.getstream.core.utils.Routes.buildPersonalizationURL; +import static io.getstream.core.utils.Serialization.*; + +public final class StreamPersonalization { + private final String key; + private final URL baseURL; + private final HTTPClient httpClient; + + StreamPersonalization(String key, URL baseURL, HTTPClient httpClient) { + this.key = key; + this.baseURL = baseURL; + this.httpClient = httpClient; + } + + public CompletableFuture> get(Token token, String userID, String resource, Map params) throws StreamException { + checkNotNull(resource, "Resource can't be empty"); + checkArgument(!resource.isEmpty(), "Resource can't be empty"); + checkNotNull(params, "Missing params"); + + try { + final URL url = buildPersonalizationURL(baseURL, resource + '/'); + final RequestOption[] options = params.entrySet().stream() + .map(entry -> new CustomQueryParameter(entry.getKey(), entry.getValue().toString())) + .toArray(RequestOption[]::new); + return httpClient.execute(buildGet(url, key, token, options)) + .thenApply(response -> { + try { + return deserialize(response, new TypeReference>() {}); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture post(Token token, String userID, String resource, Map params, Map payload) throws StreamException { + checkNotNull(resource, "Resource can't be empty"); + checkArgument(!resource.isEmpty(), "Resource can't be empty"); + checkNotNull(params, "Missing params"); + checkNotNull(params, "Missing payload"); + + try { + final byte[] jsonPayload = toJSON(new Object() { + public final Map data = payload; + }); + final URL url = buildPersonalizationURL(baseURL, resource + '/'); + final RequestOption[] options = params.entrySet().stream() + .map(entry -> new CustomQueryParameter(entry.getKey(), entry.getValue().toString())) + .toArray(RequestOption[]::new); + return httpClient.execute(buildPost(url, key, token, jsonPayload, options)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture delete(Token token, String userID, String resource, Map params) throws StreamException { + checkNotNull(resource, "Resource can't be empty"); + checkArgument(!resource.isEmpty(), "Resource can't be empty"); + checkNotNull(params, "Missing params"); + + try { + final URL url = buildPersonalizationURL(baseURL, resource + '/'); + final RequestOption[] options = params.entrySet().stream() + .map(entry -> new CustomQueryParameter(entry.getKey(), entry.getValue().toString())) + .toArray(RequestOption[]::new); + return httpClient.execute(buildDelete(url, key, token, options)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } +} diff --git a/src/main/java/io/getstream/core/StreamReactions.java b/src/main/java/io/getstream/core/StreamReactions.java new file mode 100644 index 00000000..532bb20c --- /dev/null +++ b/src/main/java/io/getstream/core/StreamReactions.java @@ -0,0 +1,168 @@ +package io.getstream.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableMap; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Token; +import io.getstream.core.models.FeedID; +import io.getstream.core.models.Reaction; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.Filter; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Request.*; +import static io.getstream.core.utils.Routes.buildReactionsURL; +import static io.getstream.core.utils.Serialization.*; + +public final class StreamReactions { + private final String key; + private final URL baseURL; + private final HTTPClient httpClient; + + StreamReactions(String key, URL baseURL, HTTPClient httpClient) { + this.key = key; + this.baseURL = baseURL; + this.httpClient = httpClient; + } + + public CompletableFuture get(Token token, String id) throws StreamException { + checkNotNull(id, "Reaction id can't be null"); + checkArgument(!id.isEmpty(), "Reaction id can't be empty"); + + try { + final URL url = buildReactionsURL(baseURL, id + '/'); + return httpClient.execute(buildGet(url, key, token)) + .thenApply(response -> { + try { + return deserialize(response, Reaction.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture> filter(Token token, LookupKind lookup, String id, Filter filter, String kind) throws StreamException { + checkNotNull(lookup, "Lookup kind can't be null"); + checkNotNull(id, "Reaction id can't be null"); + checkArgument(!id.isEmpty(), "Reaction id can't be empty"); + checkNotNull(filter, "Filter can't be null"); + checkNotNull(kind, "Kind can't be null"); + + try { + final URL url = buildReactionsURL(baseURL, lookup.getKind() + '/' + id + '/'); + return httpClient.execute(buildGet(url, key, token, filter, new CustomQueryParameter("kind", kind))) + .thenApply(response -> { + try { + return deserializeContainer(response, Reaction.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture add(Token token, String userID, Reaction reaction, FeedID... targetFeeds) throws StreamException { + checkNotNull(reaction, "Reaction can't be null"); + checkNotNull(reaction.getActivityID(), "Reaction activity id can't be null"); + checkArgument(!reaction.getActivityID().isEmpty(), "Reaction activity id can't be empty"); + checkNotNull(reaction.getKind(), "Reaction kind can't be null"); + checkArgument(!reaction.getKind().isEmpty(), "Reaction kind can't be empty"); + + String[] targetFeedIDs = Arrays.stream(targetFeeds) + .map(feed -> feed.toString()) + .toArray(String[]::new); + + try { + ImmutableMap.Builder payloadBuilder = ImmutableMap.builder(); + payloadBuilder.put("activity_id", reaction.getActivityID()); + payloadBuilder.put("kind", reaction.getKind()); + payloadBuilder.put("target_feeds", targetFeedIDs); + if (userID != null || reaction.getUserID() != null) { + payloadBuilder.put("user_id", firstNonNull(userID, reaction.getUserID())); + } + if (reaction.getId() != null) { + payloadBuilder.put("id", reaction.getId()); + } + if (reaction.getExtra() != null) { + payloadBuilder.put("data", reaction.getExtra()); + } + final byte[] payload = toJSON(payloadBuilder.build()); + final URL url = buildReactionsURL(baseURL); + return httpClient.execute(buildPost(url, key, token, payload)) + .thenApply(response -> { + try { + return deserialize(response, Reaction.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture update(Token token, Reaction reaction, FeedID... targetFeeds) throws StreamException { + checkNotNull(reaction, "Reaction can't be null"); + checkNotNull(reaction.getId(), "Reaction id can't be null"); + checkArgument(!reaction.getId().isEmpty(), "Reaction id can't be empty"); + + String[] targetFeedIDs = Arrays.stream(targetFeeds) + .map(feed -> feed.toString()) + .toArray(String[]::new); + + try { + final byte[] payload = toJSON(new Object() { + public final Map data = reaction.getExtra(); + public final String[] target_feeds = targetFeedIDs; + }); + final URL url = buildReactionsURL(baseURL, reaction.getId() + '/'); + return httpClient.execute(buildPut(url, key, token, payload)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (JsonProcessingException | MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } + + public CompletableFuture delete(Token token, String id) throws StreamException { + checkNotNull(id, "Reaction id can't be null"); + checkArgument(!id.isEmpty(), "Reaction id can't be empty"); + + try { + final URL url = buildReactionsURL(baseURL, id + '/'); + return httpClient.execute(buildDelete(url, key, token)) + .thenApply(response -> { + try { + return deserializeError(response); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } +} diff --git a/src/main/java/io/getstream/core/exceptions/StreamAPIException.java b/src/main/java/io/getstream/core/exceptions/StreamAPIException.java new file mode 100644 index 00000000..ec8f15db --- /dev/null +++ b/src/main/java/io/getstream/core/exceptions/StreamAPIException.java @@ -0,0 +1,60 @@ +package io.getstream.core.exceptions; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public final class StreamAPIException extends StreamException { + private final int errorCode; + private final int statusCode; + private final String errorName; + + @JsonCreator + public StreamAPIException( + @JsonProperty("detail") String message, + @JsonProperty("code") int errorCode, + @JsonProperty("status_code") int statusCode, + @JsonProperty("exception") String errorName) { + super(formatMessage(message, errorName, errorCode, statusCode)); + + this.errorCode = errorCode; + this.statusCode = statusCode; + this.errorName = errorName; + } + + private static String formatMessage(String message, String errorName, int errorCode, int statusCode) { + StringBuilder result = new StringBuilder(); + if (errorName != null && !errorName.isEmpty()) { + result.append(errorName); + } + if (message != null && !message.isEmpty()) { + if (result.length() > 0) { + result.append(": "); + } + + result.append(message); + } + if (result.length() > 0) { + result.append(" "); + } + result.append("(code = "); + result.append(errorCode); + result.append(" status = "); + result.append(statusCode); + result.append(')'); + return result.toString(); + } + + public int getErrorCode() { + return errorCode; + } + + public int getStatusCode() { + return statusCode; + } + + public String getErrorName() { + return errorName; + } +} diff --git a/src/main/java/io/getstream/core/exceptions/StreamException.java b/src/main/java/io/getstream/core/exceptions/StreamException.java new file mode 100644 index 00000000..e9a027d8 --- /dev/null +++ b/src/main/java/io/getstream/core/exceptions/StreamException.java @@ -0,0 +1,23 @@ +package io.getstream.core.exceptions; + +public class StreamException extends Exception { + public StreamException() { + super(); + } + + public StreamException(String message) { + super(message); + } + + public StreamException(String message, Throwable cause) { + super(message, cause); + } + + public StreamException(Throwable cause) { + super(cause); + } + + protected StreamException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/io/getstream/core/http/HTTPClient.java b/src/main/java/io/getstream/core/http/HTTPClient.java new file mode 100644 index 00000000..b699849e --- /dev/null +++ b/src/main/java/io/getstream/core/http/HTTPClient.java @@ -0,0 +1,12 @@ +package io.getstream.core.http; + +import java.util.concurrent.CompletableFuture; + +public abstract class HTTPClient { + public static Request.Builder requestBuilder() { + return Request.builder(); + } + + public abstract T getImplementation(); + public abstract CompletableFuture execute(Request request); +} diff --git a/src/main/java/io/getstream/core/http/OKHTTPClientAdapter.java b/src/main/java/io/getstream/core/http/OKHTTPClientAdapter.java new file mode 100644 index 00000000..cd3739ce --- /dev/null +++ b/src/main/java/io/getstream/core/http/OKHTTPClientAdapter.java @@ -0,0 +1,114 @@ +package io.getstream.core.http; + +import io.getstream.core.utils.Info; +import okhttp3.*; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLConnection; +import java.util.concurrent.CompletableFuture; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class OKHTTPClientAdapter extends HTTPClient { + private static final String userAgentTemplate = "okhttp3 stream-java2 %s v%s"; + + private final OkHttpClient client; + + public OKHTTPClientAdapter() { + this.client = new OkHttpClient.Builder() + .followRedirects(false) + .followSslRedirects(false) + .build(); + } + + public OKHTTPClientAdapter(OkHttpClient client) { + checkNotNull(client); + this.client = client; + } + + @Override + public T getImplementation() { + return (T) client; + } + + private okhttp3.RequestBody buildOkHttpRequestBody(io.getstream.core.http.RequestBody body) { + okhttp3.RequestBody okBody = null; + MediaType mediaType; + switch (body.getType()) { + case JSON: + mediaType = MediaType.parse(body.getType().toString()); + okBody = okhttp3.RequestBody.create(mediaType, body.getBytes()); + break; + case MULTI_PART: + String mimeType = URLConnection.guessContentTypeFromName(body.getFileName()); + mediaType = MediaType.parse(mimeType); + MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM); + if (body.getBytes() != null) { + builder.addFormDataPart("file", body.getFileName(), okhttp3.RequestBody.create(mediaType, body.getBytes())); + } else { + builder.addFormDataPart("file", body.getFileName(), okhttp3.RequestBody.create(mediaType, body.getFile())); + } + okBody = builder.build(); + break; + } + return okBody; + } + + private okhttp3.Request buildOkHttpRequest(io.getstream.core.http.Request request) { + String version = Info.getProperties().getProperty(Info.VERSION); + String userAgent = String.format(userAgentTemplate, System.getProperty("os.name"), version); + okhttp3.Request.Builder builder = new okhttp3.Request.Builder() + .url(request.getURL()) + .addHeader("Stream-Auth-Type", "jwt") + .addHeader("Authorization", request.getToken().toString()) + .addHeader("User-Agent", userAgent); + + MediaType mediaType; + switch (request.getMethod()) { + case GET: + builder.get(); + break; + case DELETE: + builder.delete(); + break; + case PUT: + builder.put(buildOkHttpRequestBody(request.getBody())); + break; + case POST: + builder.post(buildOkHttpRequestBody(request.getBody())); + break; + } + return builder.build(); + } + + private io.getstream.core.http.Response buildResponse(okhttp3.Response response) { + final InputStream body = response.body() != null ? response.body().byteStream() : null; + return new io.getstream.core.http.Response(response.code(), body); + } + + @Override + public CompletableFuture execute(io.getstream.core.http.Request request) { + final CompletableFuture result = new CompletableFuture<>(); + + client.newCall(buildOkHttpRequest(request)).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + result.completeExceptionally(e); + } + + @Override + public void onResponse(Call call, okhttp3.Response response) { + io.getstream.core.http.Response httpResponse = buildResponse(response); + try (InputStream ignored = httpResponse.getBody()){ + result.complete(httpResponse); + } catch (Exception e) { + result.completeExceptionally(e); + } + } + }); + + return result; + } +} + diff --git a/src/main/java/io/getstream/core/http/Request.java b/src/main/java/io/getstream/core/http/Request.java new file mode 100644 index 00000000..b16629db --- /dev/null +++ b/src/main/java/io/getstream/core/http/Request.java @@ -0,0 +1,157 @@ +package io.getstream.core.http; + +import com.google.common.base.MoreObjects; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Objects; + +public final class Request { + private final Token token; + private final URL url; + private final Method method; + private final RequestBody body; + + private Request(Builder builder) throws MalformedURLException { + token = builder.token; + url = builder.uri.toURL(); + method = builder.method; + body = builder.body; + } + + public Token getToken() { + return token; + } + + public URL getURL() { + return url; + } + + public Method getMethod() { + return method; + } + + public RequestBody getBody() { + return body; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request request = (Request) o; + return Objects.equals(token, request.token) && + Objects.equals(url, request.url) && + method == request.method && + Objects.equals(body, request.body); + } + + @Override + public int hashCode() { + return Objects.hash(token, url, method, body); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("token", this.token) + .add("url", this.url) + .add("method", this.method) + .add("body", this.body) + .toString(); + } + + public static Builder builder() { + return new Builder(); + } + + public enum Method { + GET, + POST, + PUT, + DELETE + } + + public static final class Builder { + private Token token; + private URI uri; + private StringBuilder query; + private Method method; + private RequestBody body; + + public Builder token(Token token) { + this.token = token; + return this; + } + + public Builder url(URL url) throws URISyntaxException { + uri = url.toURI(); + if (uri.getQuery() != null) { + query = new StringBuilder(uri.getQuery()); + } else { + query = new StringBuilder(); + } + return this; + } + + public Builder addQueryParameter(String key, String value) { + if (query.length() > 0) { + query.append('&'); + } + query.append(key); + query.append('='); + query.append(value); + return this; + } + + public Builder get() { + this.method = Method.GET; + this.body = null; + return this; + } + + public Builder post(byte[] body) { + this.method = Method.POST; + this.body = new RequestBody(body, RequestBody.Type.JSON); + return this; + } + + public Builder multiPartPost(String fileName, byte[] body) { + this.method = Method.POST; + this.body = new RequestBody(fileName, body, RequestBody.Type.MULTI_PART); + return this; + } + + public Builder multiPartPost(File body) { + this.method = Method.POST; + this.body = new RequestBody(body, RequestBody.Type.MULTI_PART); + return this; + } + + public Builder put(byte[] body) { + this.method = Method.PUT; + this.body = new RequestBody(body, RequestBody.Type.JSON); + return this; + } + + public Builder delete() { + this.method = Method.DELETE; + this.body = null; + return this; + } + + public Request build() throws MalformedURLException, URISyntaxException { + this.uri = new URI(uri.getScheme(), + uri.getUserInfo(), + uri.getHost(), + uri.getPort(), + uri.getPath(), + query.toString(), + null); + return new Request(this); + } + } +} diff --git a/src/main/java/io/getstream/core/http/RequestBody.java b/src/main/java/io/getstream/core/http/RequestBody.java new file mode 100644 index 00000000..4191803b --- /dev/null +++ b/src/main/java/io/getstream/core/http/RequestBody.java @@ -0,0 +1,93 @@ +package io.getstream.core.http; + +import com.google.common.base.MoreObjects; + +import java.io.File; +import java.util.Arrays; +import java.util.Objects; + +public class RequestBody { + public enum Type { + JSON("application/json"), + MULTI_PART("multipart/form-data"); + + private final String type; + + Type(String type) { + this.type = type; + } + + @Override + public String toString() { + return type; + } + } + + private final Type type; + private final byte[] bytes; + private final File file; + private final String fileName; + + RequestBody(byte[] bytes, Type type) { + this.type = type; + this.bytes = bytes; + this.file = null; + this.fileName = null; + } + + RequestBody(String fileName, byte[] bytes, Type type) { + this.type = type; + this.bytes = bytes; + this.file = null; + this.fileName = fileName; + } + + RequestBody(File file, Type type) { + this.type = type; + this.bytes = null; + this.file = file; + this.fileName = file.getName(); + } + + public Type getType() { + return type; + } + + public byte[] getBytes() { + return bytes; + } + + public File getFile() { + return file; + } + + public String getFileName() { + return fileName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RequestBody that = (RequestBody) o; + return type == that.type && + Arrays.equals(bytes, that.bytes) && + Objects.equals(file, that.file); + } + + @Override + public int hashCode() { + int result = Objects.hash(type, file); + result = 31 * result + Arrays.hashCode(bytes); + return result; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(RequestBody.class) + .add("type", type) + .add("bytes", bytes) + .add("file", file) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/http/Response.java b/src/main/java/io/getstream/core/http/Response.java new file mode 100644 index 00000000..c530059f --- /dev/null +++ b/src/main/java/io/getstream/core/http/Response.java @@ -0,0 +1,49 @@ +package io.getstream.core.http; + +import com.google.common.base.MoreObjects; + +import java.io.InputStream; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; + +public final class Response { + private final int code; + private final InputStream body; + + public Response(int code, InputStream body) { + checkArgument(code >= 100 && code <= 599, "Invalid HTTP status code"); + this.code = code; + this.body = body; + } + + public int getCode() { + return code; + } + + public InputStream getBody() { + return body; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Response response = (Response) o; + return code == response.code && + Objects.equals(body, response.body); + } + + @Override + public int hashCode() { + return Objects.hash(code, body); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("code", this.code) + .add("body", this.body) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/http/Token.java b/src/main/java/io/getstream/core/http/Token.java new file mode 100644 index 00000000..32a294b8 --- /dev/null +++ b/src/main/java/io/getstream/core/http/Token.java @@ -0,0 +1,35 @@ +package io.getstream.core.http; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class Token { + private final String token; + + public Token(String token) { + checkNotNull(token, "Token can't be null"); + checkArgument(!token.isEmpty(), "Token can't be null"); + + this.token = token; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Token token1 = (Token) o; + return Objects.equals(token, token1.token); + } + + @Override + public int hashCode() { + return Objects.hash(token); + } + + @Override + public String toString() { + return token; + } +} diff --git a/src/main/java/io/getstream/core/models/Activity.java b/src/main/java/io/getstream/core/models/Activity.java new file mode 100644 index 00000000..3644e633 --- /dev/null +++ b/src/main/java/io/getstream/core/models/Activity.java @@ -0,0 +1,265 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.MoreObjects; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.convert; + +@JsonInclude(Include.NON_NULL) +@JsonDeserialize(builder = Activity.Builder.class) +public class Activity { + private final String id; + private final String actor; + private final String verb; + private final String object; + private final String foreignID; + private final String target; + //TODO: support Java 8 Date/Time types? + private final Date time; + private final String origin; + private final List to; + private final Double score; + private final Map extra; + + private Activity(Builder builder) { + id = builder.id; + actor = builder.actor; + verb = builder.verb; + object = builder.object; + foreignID = builder.foreignID; + target = builder.target; + time = builder.time; + origin = builder.origin; + to = builder.to; + score = builder.score; + extra = builder.extra; + } + + public String getID() { + return id; + } + + public String getActor() { + return actor; + } + + public String getVerb() { + return verb; + } + + public String getObject() { + return object; + } + + @JsonProperty("foreign_id") + public String getForeignID() { + return foreignID; + } + + public String getTarget() { + return target; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + public Date getTime() { + return time; + } + + public String getOrigin() { + return origin; + } + + public List getTo() { + return to; + } + + public Double getScore() { + return score; + } + + @JsonAnyGetter + public Map getExtra() { + return extra; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Activity activity = (Activity) o; + return Objects.equals(id, activity.id) && + Objects.equals(actor, activity.actor) && + Objects.equals(verb, activity.verb) && + Objects.equals(object, activity.object) && + Objects.equals(foreignID, activity.foreignID) && + Objects.equals(target, activity.target) && + Objects.equals(time, activity.time) && + Objects.equals(origin, activity.origin) && + Objects.equals(to, activity.to) && + Objects.equals(score, activity.score) && + Objects.equals(extra, activity.extra); + } + + @Override + public int hashCode() { + return Objects.hash(id, actor, verb, object, foreignID, target, time, origin, to, score, extra); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", this.id) + .add("actor", this.actor) + .add("verb", this.verb) + .add("object", this.object) + .add("foreignID", this.foreignID) + .add("target", this.target) + .add("time", this.time) + .add("origin", this.origin) + .add("to", this.to) + .add("score", this.score) + .add("extra", this.extra) + .toString(); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") + public static final class Builder { + private String id; + private String actor; + private String verb; + private String object; + private String foreignID; + private String target; + private Date time; + private String origin; + private List to; + private Double score; + private Map extra; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder actor(String actor) { + this.actor = actor; + return this; + } + + public Builder verb(String verb) { + this.verb = verb; + return this; + } + + public Builder object(String object) { + this.object = object; + return this; + } + + @JsonProperty("foreign_id") + public Builder foreignID(String foreignID) { + this.foreignID = foreignID; + return this; + } + + public Builder target(String target) { + this.target = target; + return this; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + public Builder time(Date time) { + this.time = time; + return this; + } + + public Builder origin(String origin) { + this.origin = origin; + return this; + } + + @JsonProperty("to") + public Builder to(List to) { + this.to = to; + return this; + } + + @JsonIgnore + public Builder to(Iterable to) { + this.to = Lists.newArrayList(to); + return this; + } + + @JsonIgnore + public Builder to(FeedID... to) { + this.to = Lists.newArrayList(to); + return this; + } + + public Builder score(double score) { + this.score = score; + return this; + } + + @JsonAnySetter + public Builder extraField(String key, Object value) { + if (extra == null) { + extra = Maps.newHashMap(); + } + extra.put(key, value); + return this; + } + + @JsonIgnore + public Builder extra(Map extra) { + if (!extra.isEmpty()) { + this.extra = extra; + } + return this; + } + + @JsonIgnore + public Builder fromActivity(Activity activity) { + this.id = activity.id; + this.actor = activity.actor; + this.verb = activity.verb; + this.object = activity.object; + this.foreignID = activity.foreignID; + this.target = activity.target; + this.time = activity.time; + this.origin = activity.origin; + this.to = activity.to; + this.score = activity.score; + this.extra = activity.extra; + return this; + } + + @JsonIgnore + public Builder fromCustomActivity(T custom) { + return fromActivity(convert(custom, Activity.class)); + } + + public Activity build() { + checkNotNull(actor, "Activity 'actor' field required"); + checkNotNull(verb, "Activity 'verb' field required"); + checkNotNull(object, "Activity 'object' field required"); + + return new Activity(this); + } + } +} diff --git a/src/main/java/io/getstream/core/models/CollectionData.java b/src/main/java/io/getstream/core/models/CollectionData.java new file mode 100644 index 00000000..268ce4a7 --- /dev/null +++ b/src/main/java/io/getstream/core/models/CollectionData.java @@ -0,0 +1,105 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.base.MoreObjects; +import com.google.common.collect.Maps; + +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.convert; + +public class CollectionData { + private final String id; + private final String collection; + private final Map data; + + @JsonCreator + public CollectionData( + @JsonProperty("collection") + String collection, + @JsonProperty("id") + String id, + @JsonProperty("data") + Map data) { + this.collection = collection; + this.data = firstNonNull(data, Maps.newHashMap()); + this.id = checkNotNull(id, "ID required"); + } + + public CollectionData() { + this(null, "", null); + } + + public CollectionData(String id) { + this(null, id, null); + } + + public static CollectionData buildFrom(T data) { + return convert(data, CollectionData.class); + } + + public String getID() { + return id; + } + + @JsonIgnore + public String getCollection() { + return collection; + } + + @JsonAnyGetter + public Map getData() { + return data; + } + + @JsonAnySetter + public CollectionData set(String key, T value) { + checkArgument(!"id".equals(key), "Key can't be named 'id'"); + checkNotNull(key, "Key can't be null"); + checkNotNull(value, "Value can't be null"); + + data.put(key, value); + return this; + } + + public CollectionData from(T data) { + checkNotNull(data, "Can't extract data from null"); + + Map map = convert(data, new TypeReference>() {}); + for (Map.Entry entry : map.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + return this; + } + + public T get(String key) { + return (T) data.get(checkNotNull(key, "Key can't be null")); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CollectionData collectionData = (CollectionData) o; + return Objects.equals(id, collectionData.id) && + Objects.equals(data, collectionData.data); + } + + @Override + public int hashCode() { + return Objects.hash(id, data); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", this.id) + .add("data", this.data) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/models/Content.java b/src/main/java/io/getstream/core/models/Content.java new file mode 100644 index 00000000..0932c8d5 --- /dev/null +++ b/src/main/java/io/getstream/core/models/Content.java @@ -0,0 +1,86 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.base.MoreObjects; +import com.google.common.collect.Maps; + +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.convert; + +public class Content { + private final String foreignID; + private final Map data = Maps.newHashMap(); + + @JsonCreator + public Content(@JsonProperty("foreign_id") String foreignID) { + this.foreignID = checkNotNull(foreignID, "ID required"); + } + + public static Content buildFrom(T data) { + return convert(data, Content.class); + } + + @JsonProperty("foreign_id") + public String getForeignID() { + return foreignID; + } + + @JsonAnyGetter + public Map getData() { + return data; + } + + @JsonAnySetter + public Content set(String key, T value) { + checkArgument(!"foreignID".equals(key), "Key can't be named 'foreignID'"); + checkNotNull(key, "Key can't be null"); + checkNotNull(value, "Value can't be null"); + + data.put(key, value); + return this; + } + + public Content from(T data) { + checkNotNull(data, "Can't extract data from null"); + + Map map = convert(data, new TypeReference>() {}); + for (Map.Entry entry : map.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + return this; + } + + public T get(String key) { + return (T) data.get(checkNotNull(key, "Key can't be null")); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Content collectionData = (Content) o; + return Objects.equals(foreignID, collectionData.foreignID) && + Objects.equals(data, collectionData.data); + } + + @Override + public int hashCode() { + return Objects.hash(foreignID, data); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", this.foreignID) + .add("data", this.data) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/models/Data.java b/src/main/java/io/getstream/core/models/Data.java new file mode 100644 index 00000000..25489875 --- /dev/null +++ b/src/main/java/io/getstream/core/models/Data.java @@ -0,0 +1,86 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.MoreObjects; +import com.google.common.collect.Maps; +import io.getstream.core.models.serialization.DataDeserializer; + +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.convert; + +@JsonDeserialize(using = DataDeserializer.class) +public class Data { + private final String id; + private final Map data = Maps.newHashMap(); + + public Data(String id) { + this.id = checkNotNull(id, "ID required"); + } + + public static Data buildFrom(T data) { + return convert(data, Data.class); + } + + public String getID() { + return id; + } + + @JsonAnyGetter + public Map getData() { + return data; + } + + public Data set(String key, T value) { + checkArgument(!"id".equals(key), "Key can't be named 'id'"); + checkNotNull(key, "Key can't be null"); + checkNotNull(value, "Value can't be null"); + + data.put(key, value); + return this; + } + + public Data from(T data) { + return from(convert(data, new TypeReference>() {})); + } + + public Data from(Map map) { + checkNotNull(data, "Can't extract data from null"); + + for (Map.Entry entry : map.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + return this; + } + + public T get(String key) { + return (T) data.get(checkNotNull(key, "Key can't be null")); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Data data = (Data) o; + return Objects.equals(id, data.id) && + Objects.equals(data, data.data); + } + + @Override + public int hashCode() { + return Objects.hash(id, data); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", this.id) + .add("data", this.data) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/models/Engagement.java b/src/main/java/io/getstream/core/models/Engagement.java new file mode 100644 index 00000000..faace27a --- /dev/null +++ b/src/main/java/io/getstream/core/models/Engagement.java @@ -0,0 +1,210 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.MoreObjects; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.convert; + +@JsonInclude(Include.NON_NULL) +@JsonDeserialize(builder = Engagement.Builder.class) +public class Engagement { + private final String feedID; + private final UserData userData; + private final String label; + private final Content content; + private final Integer boost; + private final Integer position; + private final String location; + private final List features; + private final Date trackedAt; + + private Engagement(Builder builder) { + label = builder.label; + content = builder.content; + boost = builder.boost; + position = builder.position; + feedID = builder.feedID; + location = builder.location; + userData = builder.userData; + features = builder.features; + trackedAt = builder.trackedAt; + } + + public static Builder builder() { + return new Builder(); + } + + public String getLabel() { + return label; + } + + public Content getContent() { + return content; + } + + public int getBoost() { + return boost; + } + + public int getPosition() { + return position; + } + + @JsonProperty("feed_id") + public String getFeedID() { + return feedID; + } + + public String getLocation() { + return location; + } + + @JsonProperty("user_data") + public UserData getUserData() { + return userData; + } + + public List getFeatures() { + return features; + } + + @JsonProperty("tracked_at") + public Date getTrackedAt() { + return trackedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Engagement that = (Engagement) o; + return Objects.equals(label, that.label) && + Objects.equals(content, that.content) && + Objects.equals(boost, that.boost) && + Objects.equals(position, that.position) && + Objects.equals(feedID, that.feedID) && + Objects.equals(location, that.location) && + Objects.equals(userData, that.userData) && + Objects.equals(features, that.features) && + Objects.equals(trackedAt, that.trackedAt); + } + + @Override + public int hashCode() { + return Objects.hash(label, content, boost, position, feedID, location, userData, features, trackedAt); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("label", this.label) + .add("content", this.content) + .add("boost", this.boost) + .add("position", this.position) + .add("feedID", this.feedID) + .add("location", this.location) + .add("userData", this.userData) + .add("features", this.features) + .add("trackedAt", this.trackedAt) + .toString(); + } + + @JsonPOJOBuilder(withPrefix = "") + public static final class Builder { + private String label; + private Content content; + private Integer boost; + private Integer position; + private String feedID; + private String location; + private UserData userData; + private List features; + private Date trackedAt; + + public Builder label(String label) { + this.label = label; + return this; + } + + public Builder content(Content content) { + this.content = content; + return this; + } + + public Builder boost(int boost) { + this.boost = boost; + return this; + } + + public Builder position(int position) { + this.position = position; + return this; + } + + @JsonProperty("feed_id") + public Builder feedID(String feedID) { + this.feedID = feedID; + return this; + } + + public Builder location(String location) { + this.location = location; + return this; + } + + @JsonProperty("user_data") + public Builder userData(UserData userData) { + this.userData = userData; + return this; + } + + public Builder features(List features) { + this.features = features; + return this; + } + + @JsonProperty("tracked_at") + public Builder trackedAt(Date trackedAt) { + this.trackedAt = trackedAt; + return this; + } + + @JsonIgnore + public Builder fromEngagement(Engagement engagement) { + label = engagement.label; + content = engagement.content; + boost = engagement.boost; + position = engagement.position; + feedID = engagement.feedID; + location = engagement.location; + userData = engagement.userData; + features = engagement.features; + trackedAt = engagement.trackedAt; + return this; + } + + @JsonIgnore + public Builder fromCustomEngagement(T custom) { + return fromEngagement(convert(custom, Engagement.class)); + } + + public Engagement build() { + checkNotNull(feedID, "Engagement 'feedID' field required"); + checkNotNull(userData, "Engagement 'userData' field required"); + checkNotNull(label, "Engagement 'label' field required"); + checkNotNull(content, "Engagement 'content' field required"); + + return new Engagement(this); + } + } +} diff --git a/src/main/java/io/getstream/core/models/EnrichedActivity.java b/src/main/java/io/getstream/core/models/EnrichedActivity.java new file mode 100644 index 00000000..b777ecf8 --- /dev/null +++ b/src/main/java/io/getstream/core/models/EnrichedActivity.java @@ -0,0 +1,300 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.MoreObjects; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.convert; + +@JsonInclude(Include.NON_NULL) +@JsonDeserialize(builder = EnrichedActivity.Builder.class) +public class EnrichedActivity { + private final String id; + private final Data actor; + private final Data verb; + private final Data object; + private final String foreignID; + private final Data target; + //TODO: support Java 8 Date/Time types? + private final Date time; + private final Data origin; + private final List to; + private final Double score; + private final Map extra; + + private EnrichedActivity(Builder builder) { + id = builder.id; + actor = builder.actor; + verb = builder.verb; + object = builder.object; + foreignID = builder.foreignID; + target = builder.target; + time = builder.time; + origin = builder.origin; + to = builder.to; + score = builder.score; + extra = builder.extra; + } + + public String getID() { + return id; + } + + public Data getActor() { + return actor; + } + + public Data getVerb() { + return verb; + } + + public Data getObject() { + return object; + } + + @JsonProperty("foreign_id") + public String getForeignID() { + return foreignID; + } + + public Data getTarget() { + return target; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + public Date getTime() { + return time; + } + + public Data getOrigin() { + return origin; + } + + public List getTo() { + return to; + } + + public Double getScore() { + return score; + } + + @JsonAnyGetter + public Map getExtra() { + return extra; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EnrichedActivity activity = (EnrichedActivity) o; + return Objects.equals(id, activity.id) && + Objects.equals(actor, activity.actor) && + Objects.equals(verb, activity.verb) && + Objects.equals(object, activity.object) && + Objects.equals(foreignID, activity.foreignID) && + Objects.equals(target, activity.target) && + Objects.equals(time, activity.time) && + Objects.equals(origin, activity.origin) && + Objects.equals(to, activity.to) && + Objects.equals(score, activity.score) && + Objects.equals(extra, activity.extra); + } + + @Override + public int hashCode() { + return Objects.hash(id, actor, verb, object, foreignID, target, time, origin, to, score, extra); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", this.id) + .add("actor", this.actor) + .add("verb", this.verb) + .add("object", this.object) + .add("foreignID", this.foreignID) + .add("target", this.target) + .add("time", this.time) + .add("origin", this.origin) + .add("to", this.to) + .add("score", this.score) + .add("extra", this.extra) + .toString(); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") + public static final class Builder { + private String id; + private Data actor; + private Data verb; + private Data object; + private String foreignID; + private Data target; + private Date time; + private Data origin; + private List to; + private Double score; + private Map extra; + + public Builder id(String id) { + this.id = id; + return this; + } + + @JsonIgnore + public Builder actor(String actor) { + this.actor = new Data(actor); + return this; + } + + @JsonProperty("actor") + public Builder actor(Data actor) { + this.actor = actor; + return this; + } + + @JsonIgnore + public Builder verb(String verb) { + this.verb = new Data(verb); + return this; + } + + @JsonProperty("verb") + public Builder verb(Data verb) { + this.verb = verb; + return this; + } + + @JsonIgnore + public Builder object(String object) { + this.object = new Data(object); + return this; + } + + @JsonProperty("object") + public Builder object(Data object) { + this.object = object; + return this; + } + + @JsonProperty("foreign_id") + public Builder foreignID(String foreignID) { + this.foreignID = foreignID; + return this; + } + + @JsonIgnore + public Builder target(String target) { + this.target = new Data(target); + return this; + } + + @JsonProperty("target") + public Builder target(Data target) { + this.target = target; + return this; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + public Builder time(Date time) { + this.time = time; + return this; + } + + @JsonIgnore + public Builder origin(String origin) { + this.origin = new Data(origin); + return this; + } + + @JsonProperty("origin") + public Builder origin(Data origin) { + this.origin = origin; + return this; + } + + @JsonProperty("to") + public Builder to(List to) { + this.to = to; + return this; + } + + @JsonIgnore + public Builder to(Iterable to) { + this.to = Lists.newArrayList(to); + return this; + } + + @JsonIgnore + public Builder to(FeedID... to) { + this.to = Lists.newArrayList(to); + return this; + } + + public Builder score(double score) { + this.score = score; + return this; + } + + @JsonAnySetter + public Builder extraField(String key, Object value) { + if (extra == null) { + extra = Maps.newHashMap(); + } + extra.put(key, value); + return this; + } + + @JsonIgnore + public Builder extra(Map extra) { + if (!extra.isEmpty()) { + this.extra = extra; + } + return this; + } + + @JsonIgnore + public Builder fromEnrichedActivity(EnrichedActivity activity) { + this.id = activity.id; + this.actor = activity.actor; + this.verb = activity.verb; + this.object = activity.object; + this.foreignID = activity.foreignID; + this.target = activity.target; + this.time = activity.time; + this.origin = activity.origin; + this.to = activity.to; + this.score = activity.score; + this.extra = activity.extra; + return this; + } + + @JsonIgnore + public Builder fromCustomEnrichedActivity(T custom) { + return fromEnrichedActivity(convert(custom, EnrichedActivity.class)); + } + + public EnrichedActivity build() { + checkNotNull(actor, "EnrichedActivity 'actor' field required"); + checkNotNull(verb, "EnrichedActivity 'verb' field required"); + checkNotNull(object, "EnrichedActivity 'object' field required"); + + return new EnrichedActivity(this); + } + } +} diff --git a/src/main/java/io/getstream/core/models/Feature.java b/src/main/java/io/getstream/core/models/Feature.java new file mode 100644 index 00000000..641535eb --- /dev/null +++ b/src/main/java/io/getstream/core/models/Feature.java @@ -0,0 +1,48 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +import java.util.Objects; + +public class Feature { + private final String group; + private final String value; + + @JsonCreator + public Feature(@JsonProperty("group") String group, @JsonProperty("value") String value) { + this.group = group; + this.value = value; + } + + public String getGroup() { + return group; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Feature feature = (Feature) o; + return Objects.equals(group, feature.group) && + Objects.equals(value, feature.value); + } + + @Override + public int hashCode() { + return Objects.hash(group, value); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("group", this.group) + .add("value", this.value) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/models/FeedID.java b/src/main/java/io/getstream/core/models/FeedID.java new file mode 100644 index 00000000..680609ca --- /dev/null +++ b/src/main/java/io/getstream/core/models/FeedID.java @@ -0,0 +1,62 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@JsonSerialize(using = ToStringSerializer.class) +public final class FeedID { + private final String slug; + private final String userID; + + public FeedID(String slug, String userID) { + checkNotNull(slug, "Feed slug can't be null"); + checkArgument(!slug.contains(":"), "Invalid slug"); + checkNotNull(userID, "Feed user ID can't be null"); + checkArgument(!userID.contains(":"), "Invalid user ID"); + + this.slug = slug; + this.userID = userID; + } + + public FeedID(String id) { + checkNotNull(id, "Feed ID can't be null"); + checkArgument(id.contains(":"), "Invalid feed ID"); + + String[] parts = id.split(":"); + checkArgument(parts.length == 2, "Invalid feed ID"); + this.slug = parts[0]; + this.userID = parts[1]; + } + + public String getSlug() { + return slug; + } + + public String getUserID() { + return userID; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeedID feedID = (FeedID) o; + return Objects.equals(slug, feedID.slug) && + Objects.equals(userID, feedID.userID); + } + + @Override + public int hashCode() { + return Objects.hash(slug, userID); + } + + @Override + public String toString() { + return slug + ':' + userID; + } +} diff --git a/src/main/java/io/getstream/core/models/FollowRelation.java b/src/main/java/io/getstream/core/models/FollowRelation.java new file mode 100644 index 00000000..6383b5cc --- /dev/null +++ b/src/main/java/io/getstream/core/models/FollowRelation.java @@ -0,0 +1,55 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +@JsonIgnoreProperties(ignoreUnknown = true) +public final class FollowRelation { + private final String source; + private final String target; + + @JsonCreator + public FollowRelation(@JsonProperty("feed_id") String source, @JsonProperty("target_id") String target) { + checkNotNull(source, "FollowRelation 'source' field required"); + checkNotNull(target, "FollowRelation 'target' field required"); + + this.source = source; + this.target = target; + } + + public String getSource() { + return this.source; + } + + public String getTarget() { + return this.target; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FollowRelation that = (FollowRelation) o; + return Objects.equals(source, that.source) && + Objects.equals(target, that.target); + } + + @Override + public int hashCode() { + return Objects.hash(source, target); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("source", this.source) + .add("target", this.target) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/models/ForeignIDTimePair.java b/src/main/java/io/getstream/core/models/ForeignIDTimePair.java new file mode 100644 index 00000000..0d8b65fa --- /dev/null +++ b/src/main/java/io/getstream/core/models/ForeignIDTimePair.java @@ -0,0 +1,46 @@ +package io.getstream.core.models; + +import com.google.common.base.MoreObjects; + +import java.util.Date; +import java.util.Objects; + +public final class ForeignIDTimePair { + private final String foreignID; + private final Date time; + + public ForeignIDTimePair(String foreignID, Date time) { + this.foreignID = foreignID; + this.time = time; + } + + public String getForeignID() { + return foreignID; + } + + public Date getTime() { + return time; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ForeignIDTimePair that = (ForeignIDTimePair) o; + return Objects.equals(foreignID, that.foreignID) && + Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(foreignID, time); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("foreignID", this.foreignID) + .add("time", this.time) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/models/Group.java b/src/main/java/io/getstream/core/models/Group.java new file mode 100644 index 00000000..25679ee4 --- /dev/null +++ b/src/main/java/io/getstream/core/models/Group.java @@ -0,0 +1,93 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Group { + private final String group; + private final List activities; + private final int actorCount; + private final Date createdAt; + private final Date updatedAt; + + @JsonCreator + public Group( + @JsonProperty("group") + String group, + @JsonProperty("activities") + List activities, + @JsonProperty("actor_count") + int actorCount, + @JsonProperty("created_at") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + Date createdAt, + @JsonProperty("updated_at") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + Date updatedAt) { + checkNotNull(group, "Group 'group' field required"); + checkNotNull(activities, "Group 'activities' field required"); + + this.group = group; + this.activities = activities; + this.actorCount = actorCount; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public String getGroup() { + return group; + } + + public List getActivities() { + return activities; + } + + public int getActorCount() { + return actorCount; + } + + public Date getCreatedAt() { + return createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Group that = (Group) o; + return actorCount == that.actorCount && + Objects.equals(group, that.group) && + Objects.equals(activities, that.activities) && + Objects.equals(createdAt, that.createdAt) && + Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(group, activities, actorCount, createdAt, updatedAt); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("activities", this.activities) + .add("actorCount", this.actorCount) + .add("createdAt", this.createdAt) + .add("updatedAt", this.updatedAt) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/models/Impression.java b/src/main/java/io/getstream/core/models/Impression.java new file mode 100644 index 00000000..53c86292 --- /dev/null +++ b/src/main/java/io/getstream/core/models/Impression.java @@ -0,0 +1,180 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.base.MoreObjects; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.getstream.core.utils.Serialization.convert; + +@JsonInclude(Include.NON_NULL) +@JsonDeserialize(builder = Impression.Builder.class) +public class Impression { + private final String feedID; + private final UserData userData; + private final List contentList; + private final String position; + private final String location; + private final List features; + private final Date trackedAt; + + private Impression(Builder builder) { + position = builder.position; + feedID = builder.feedID; + location = builder.location; + userData = builder.userData; + contentList = builder.contentList; + features = builder.features; + trackedAt = builder.trackedAt; + } + + public static Builder builder() { + return new Builder(); + } + + public String getPosition() { + return position; + } + + @JsonProperty("feed_id") + public String getFeedID() { + return feedID; + } + + public String getLocation() { + return location; + } + + @JsonProperty("user_data") + public UserData getUserData() { + return userData; + } + + @JsonProperty("content_list") + public List getContentList() { + return contentList; + } + + public List getFeatures() { + return features; + } + + @JsonProperty("tracked_at") + public Date getTrackedAt() { + return trackedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Impression that = (Impression) o; + return Objects.equals(position, that.position) && + Objects.equals(feedID, that.feedID) && + Objects.equals(location, that.location) && + Objects.equals(userData, that.userData) && + Objects.equals(contentList, that.contentList) && + Objects.equals(features, that.features) && + Objects.equals(trackedAt, that.trackedAt); + } + + @Override + public int hashCode() { + return Objects.hash(position, feedID, location, userData, contentList, features, trackedAt); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("position", this.position) + .add("feedID", this.feedID) + .add("location", this.location) + .add("userData", this.userData) + .add("contentList", this.contentList) + .add("features", this.features) + .add("trackedAt", this.trackedAt) + .toString(); + } + + @JsonPOJOBuilder(withPrefix = "") + public static final class Builder { + private String position; + private String feedID; + private String location; + private UserData userData; + private List contentList; + private List features; + private Date trackedAt; + + public Builder position(String position) { + this.position = position; + return this; + } + + @JsonProperty("feed_id") + public Builder feedID(String feedID) { + this.feedID = feedID; + return this; + } + + public Builder location(String location) { + this.location = location; + return this; + } + + @JsonProperty("user_data") + public Builder userData(UserData userData) { + this.userData = userData; + return this; + } + + @JsonProperty("user_data") + public Builder contentList(List contentList) { + this.contentList = contentList; + return this; + } + + public Builder features(List features) { + this.features = features; + return this; + } + + @JsonProperty("tracked_at") + public Builder trackedAt(Date trackedAt) { + this.trackedAt = trackedAt; + return this; + } + + @JsonIgnore + public Builder fromImpression(Impression impression) { + position = impression.position; + feedID = impression.feedID; + location = impression.location; + userData = impression.userData; + contentList = impression.contentList; + features = impression.features; + trackedAt = impression.trackedAt; + return this; + } + + @JsonIgnore + public Builder fromCustomImpression(T custom) { + return fromImpression(convert(custom, Impression.class)); + } + + public Impression build() { + checkNotNull(feedID, "Impression 'feedID' field required"); + checkNotNull(userData, "Impression 'userData' field required"); + + return new Impression(this); + } + } +} diff --git a/src/main/java/io/getstream/core/models/NotificationGroup.java b/src/main/java/io/getstream/core/models/NotificationGroup.java new file mode 100644 index 00000000..7c47ada5 --- /dev/null +++ b/src/main/java/io/getstream/core/models/NotificationGroup.java @@ -0,0 +1,76 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class NotificationGroup extends Group { + private final boolean seen; + private final boolean read; + + @JsonCreator + public NotificationGroup( + @JsonProperty("group") + String group, + @JsonProperty("activities") + List activities, + @JsonProperty("actor_count") + int actorCount, + @JsonProperty("created_at") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + Date createdAt, + @JsonProperty("updated_at") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.S", timezone = "UTC") + Date updatedAt, + @JsonProperty("is_seen") + boolean isSeen, + @JsonProperty("is_read") + boolean isRead) { + super(group, activities, actorCount, createdAt, updatedAt); + + this.seen = isSeen; + this.read = isRead; + } + + public boolean isSeen() { + return seen; + } + + public boolean isRead() { + return read; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + NotificationGroup that = (NotificationGroup) o; + return seen == that.seen && + read == that.read; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), seen, read); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("activities", getActivities()) + .add("actorCount", getActorCount()) + .add("createdAt", getCreatedAt()) + .add("updatedAt", getUpdatedAt()) + .add("isSeen", seen) + .add("isRead", read) + .toString(); + } +} diff --git a/src/main/java/io/getstream/core/models/OGData.java b/src/main/java/io/getstream/core/models/OGData.java new file mode 100644 index 00000000..d010141f --- /dev/null +++ b/src/main/java/io/getstream/core/models/OGData.java @@ -0,0 +1,271 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class OGData { + public static class Image { + private final String image; + private final String url; + private final String secureUrl; + private final String width; + private final String height; + private final String type; + private final String alt; + + @JsonCreator + public Image( + @JsonProperty("image") + String image, + @JsonProperty("url") + String url, + @JsonProperty("secure_url") + String secureUrl, + @JsonProperty("width") + String width, + @JsonProperty("height") + String height, + @JsonProperty("type") + String type, + @JsonProperty("alt") + String alt) { + this.image = image; + this.url = url; + this.secureUrl = secureUrl; + this.width = width; + this.height = height; + this.type = type; + this.alt = alt; + } + + public String getImage() { + return image; + } + + public String getURL() { + return url; + } + + public String getSecureUrl() { + return secureUrl; + } + + public String getWidth() { + return width; + } + + public String getHeight() { + return height; + } + + public String getType() { + return type; + } + + public String getAlt() { + return alt; + } + } + + public static class Video { + private final String video; + private final String alt; + private final String url; + private final String secureURL; + private final String type; + private final String width; + private final String height; + + @JsonCreator + public Video( + @JsonProperty("video") + String video, + @JsonProperty("alt") + String alt, + @JsonProperty("url") + String url, + @JsonProperty("secure_url") + String secureURL, + @JsonProperty("type") + String type, + @JsonProperty("width") + String width, + @JsonProperty("height") + String height) { + this.video = video; + this.alt = alt; + this.url = url; + this.secureURL = secureURL; + this.type = type; + this.width = width; + this.height = height; + } + + public String getSecureURL() { + return secureURL; + } + + public String getURL(){ + return url; + } + + public String getWidth() { + return width; + } + + public String getHeight() { + return height; + } + + public String getType() { + return type; + } + + public String getAlt() { + return alt; + } + + public String getVideo() { + return video; + } + } + + public static class Audio { + private final String url; + private final String secureURL; + private final String type; + private final String audio; + + @JsonCreator + public Audio( + @JsonProperty("url") + String url, + @JsonProperty("secure_url") + String secureURL, + @JsonProperty("type") + String type, + @JsonProperty("audio") + String audio) { + this.type = type; + this.audio = audio; + this.url = url; + this.secureURL = secureURL; + } + + public String getSecureURL() { + return secureURL; + } + + public String getURL(){ + return url; + } + + public String getType() { + return type; + } + + public String getAudio() { + return audio; + } + } + + private final String title; + private final String type; + private final String description; + private final String determiner; + private final String locale; + private final String siteName; + private final List images; + private final List