Android MVVM architecture and observing changes on data from an API - java

I'm new to the Android MVVM architecture. I have an API running locally with data ("deals") in it. I'd like to simply make a request to the API and display that data in a text field. Currently the data does not show up when the fragment is first loaded, but if I go to another activity and then back to the fragment it loads.
There are 3 classes of importance here.
DashboardViewModel.java:
package com.example.android_client.ui.dashboard;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.android_client.models.Deal;
import com.example.android_client.repository.Repository;
import java.util.List;
public class DashboardViewModel extends ViewModel {
private MutableLiveData<String> mText;
private Repository repository;
private MutableLiveData<List<Deal>> deals = null;
public void init() {
if(this.deals == null) {
this.repository = Repository.getInstance();
this.deals = this.repository.getDeals();
}
}
public DashboardViewModel() {
this.mText = new MutableLiveData<>();
}
public LiveData<List<Deal>> getDeals() {
return this.deals;
}
}
DashboardFragment.java:
package com.example.android_client.ui.dashboard;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import com.example.android_client.R;
import com.example.android_client.models.Deal;
import java.util.List;
public class DashboardFragment extends Fragment {
private DashboardViewModel dashboardViewModel;
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
final TextView textView = root.findViewById(R.id.text_dashboard);
dashboardViewModel = ViewModelProviders.of(this).get(DashboardViewModel.class);
dashboardViewModel.init();
dashboardViewModel.getDeals().observe(this, new Observer<List<Deal>>() {
#Override
public void onChanged(List<Deal> deals) {
if (deals != null && !deals.isEmpty()) {
System.out.println(deals.get(0).toString());
textView.setText(deals.get(0).toString());
}
}
});
return root;
}
}
and Repository.java:
package com.example.android_client.repository;
import androidx.lifecycle.MutableLiveData;
import com.example.android_client.models.Deal;
import com.google.gson.Gson;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class Repository {
private static Repository instance;
private ArrayList<Deal> dealsList = new ArrayList<>();
private final OkHttpClient client = new OkHttpClient();
public static Repository getInstance() {
if(instance == null) {
instance = new Repository();
}
return instance;
}
private Repository() {}
public MutableLiveData<List<Deal>> getDeals() {
setDeals();
MutableLiveData<List<Deal>> deals = new MutableLiveData<>();
deals.setValue(dealsList);
return deals;
}
private void setDeals() {
Request request = new Request.Builder()
.url("http://10.0.2.2:8000/api/deals?<params here>")
.build();
client.newCall(request).enqueue(new Callback() {
#Override
public void onFailure(#NotNull Call call, #NotNull IOException e) {
e.printStackTrace();
}
#Override
public void onResponse(#NotNull Call call, #NotNull Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
String jsonDeals = responseBody.string(); // can only call string() once or you'll get an IllegalStateException
Deal[] deals = new Gson().fromJson(jsonDeals, Deal[].class);
dealsList = new ArrayList<>(Arrays.asList(deals));
}
}
});
}
}
When stepping through the code in the Repository class I can see that setDeals() is called when I load the fragment, and the request in the callback is queued. The first time getDeals() returns, it returns a list of 0 deals (within the MutableLiveData object).
onResponse in the callback doesn't run until the fragment is already loaded. When debugging I can see that the data is in the objects (all the Gson stuff works fine), but onChanged doesn't get called again (which sets the text view).
Am I not observing changes on the deals properly?

Your code is not working due to a new live data instance be created whenever getDeals() is called and the api response value be informed to other live data instance. You must set api response value to same instance of MutableLiveData returned by getDeals()
I'm not saying that it is the best architectural solution, but if you create a mutable live data as a class attribute and return it whenever getDeals() is called. Probably, it's going to work.
Also, a good practice is return a LiveData and not a MutableLiveData to not allowing a external component modify the internal value.
Please, take a look at the piece of code below.
OBS: Maybe, there is some syntax error, because I have not compiled it
import com.example.android_client.models.Deal;
import com.google.gson.Gson;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class Repository {
private static Repository instance;
private ArrayList<Deal> dealsList = new ArrayList<>();
private final OkHttpClient client = new OkHttpClient();
private MutableLiveData<List<Deal>> _deals = new MutableLiveData<>();
private LiveData<List<Deal>> deals = _deals
public static Repository getInstance() {
if(instance == null) {
instance = new Repository();
}
return instance;
}
private Repository() {}
public LiveData<List<Deal>> getDeals() {
setDeals();
return deals;
}
private void setDeals() {
Request request = new Request.Builder()
.url("http://10.0.2.2:8000/api/deals?<params here>")
.build();
client.newCall(request).enqueue(new Callback() {
#Override
public void onFailure(#NotNull Call call, #NotNull IOException e) {
e.printStackTrace();
}
#Override
public void onResponse(#NotNull Call call, #NotNull Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
String jsonDeals = responseBody.string(); // can only call string() once or you'll get an IllegalStateException
Deal[] deals = new Gson().fromJson(jsonDeals, Deal[].class);
dealsList = new ArrayList<>(Arrays.asList(deals));
_deals.setValue(dealsList);
}
}
});
}
}
When

I think this would help. Try postValue on MutableLiveData in onResponse of network call. Please change your repository class like below:
package com.example.android_client.repository;
import androidx.lifecycle.MutableLiveData;
import com.example.android_client.models.Deal;
import com.google.gson.Gson;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class Repository {
private static Repository instance;
private ArrayList<Deal> dealsList = new ArrayList<>();
private final OkHttpClient client = new OkHttpClient();
MutableLiveData<List<Deal>> deals = new MutableLiveData<>();
public static Repository getInstance() {
if(instance == null) {
instance = new Repository();
}
return instance;
}
private Repository() {}
private MutableLiveData<List<Deal>> getDeals() {
Request request = new Request.Builder()
.url("http://10.0.2.2:8000/api/deals?<params here>")
.build();
client.newCall(request).enqueue(new Callback() {
#Override
public void onFailure(#NotNull Call call, #NotNull IOException e) {
e.printStackTrace();
}
#Override
public void onResponse(#NotNull Call call, #NotNull Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
String jsonDeals = responseBody.string(); // can only call string() once or you'll get an IllegalStateException
Deal[] deals = new Gson().fromJson(jsonDeals, Deal[].class);
dealsList = new ArrayList<>(Arrays.asList(deals));
deals.postValue(dealsList);
}
}
});
return deals;
}
}

in your repository class in function get deals. you are initializing live data. requesting url in background thread and posting value on live data which is not received from server yet.
to solve this create livedata instance in constructor of repository and postvalue on livedata in onResponse callback.
//sorry for bad writting, posted from mobile.

Related

Springboot - Save entity in 'normal' class

I'm pretty new to Springboot and Java in general and because we got this in school I'm fiddeling arround.
I'm now trying to save an entity outside of the Springboot Entities, Repositories or RestController with the following code:
InfMApplication.java:
package com.domain.springboot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.domain.springboot.repositories.MovieRepository;
import com.domain.springboot.services.MovieImport;
#SpringBootApplication
public class InfMApplication {
public static void main(String[] args) {
SpringApplication.run(InfMApplication.class, args);
MovieImport movieImport = new MovieImport();
movieImport.saveToDb();
}
}
MovieImport.java:
package com.domain.springboot.services;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.CrossOrigin;
import java.io.*;
import java.net.URL;
import com.google.gson.Gson;
import com.domain.omdbapi.entities.Movie;
import com.domain.omdbapi.entities.SearchResponse;
import com.domain.omdbapi.entities.SearchResult;
import com.domain.springboot.repositories.ComplexRepository;
import com.domain.springboot.repositories.DocumentRepository;
import com.domain.springboot.repositories.MovieRepository;
import com.domain.springboot.repositories.SimpleRepository;
#Service
public class MovieImport {
private final MovieRepository movieRepository;
public MovieImport(MovieRepository movieRepository){
this.movieRepository = movieRepository;
}
public void main() {
String randomImdbId = fetchRandomMovie();
Movie movie = fetchMovieDetails(randomImdbId);
saveToDb(movie);
}
public void saveToDb(Movie movie) {
com.domain.springboot.entities.Movie springbootMovie = new com.domain.springboot.entities.Movie(movie.Title, movie.imdbID);
this.movieRepository.save(springbootMovie);
}
public String fetchRandomMovie() {
String randomWord = getRandomWord();
String url = "https://www.omdbapi.com/?apikey=<API_KEY>&type=movie&s=" + randomWord;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(
URI.create(url))
.header("accept", "application/json")
.build();
HttpResponse<String> response = null;
try {
response = client.send(request, BodyHandlers.ofString());
} catch (Exception e) {
System.out.println(e);
}
Gson gson = new Gson();
SearchResponse searchResponse = gson.fromJson(response.body(), SearchResponse.class);
int randomIndex = new Random().nextInt(0, searchResponse.getSearch().length);
SearchResult randomResult = searchResponse.getSearch()[randomIndex];
return randomResult.getImdbID();
}
public Movie fetchMovieDetails(String imdbId) {
String url = "https://www.omdbapi.com/?apikey=<API_KEY>&type=movie&plot=full&i=" + imdbId;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(
URI.create(url))
.header("accept", "application/json")
.build();
HttpResponse<String> response = null;
try {
response = client.send(request, BodyHandlers.ofString());
} catch (Exception e) {
System.out.println(e);
}
Gson gson = new Gson();
Movie movie = gson.fromJson(response.body(), Movie.class);
return movie;
}
public String getRandomWord() {
URL resource = getClass().getClassLoader().getResource("Wordlist.txt");
List<String> words = new ArrayList<>();
try {
File file = new File(resource.toURI());
words = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
int randomIndex = new Random().nextInt(0, words.size());
return words.get(randomIndex);
}
}
If I use "this.movieRepository.save(movieObject);" to save a movie in the MovieRestController the same way, it works. I also tried adding the "#Autowire" annotation, but this didn't work.
I always get the error
java.lang.NullPointerException: Cannot invoke "com.domain.springboot.repositories.MovieRepository.save(Object)" because "this.movieRepository" is null
How can I get to use the movieRepository in other Java classes like in the RestControllers?
java.lang.NullPointerException: Cannot invoke
"com.domain.springboot.repositories.MovieRepository.save(Object)"
because "this.movieRepository" is null
Above is perfectly valid if we look at your following shared code.
public class MovieImport {
private MovieRepository movieRepository;
public void saveToDb() {
// Create movie
com.domain.springboot.entities.Movie springbootMovie = new com.domain.springboot.entities.Movie("Iron Man", "284cb8fgf");
this.movieRepository.save(springbootMovie);
}
}
You've to correct certain things in your code base.
First you're not initializing the movieRepository and therefore, you're getting the null pointer exception. As you've been using the springboot you can use construction injection to initialized the field by spring container. Also. this class should be scanned by spring and you should also put some annotation such as Component or Service on top of it.
Following will work if your MovieImport and MovieRepository classess will scan by springboot.
package com.domain;
import com.domain.omdbapi.entities.Movie;
import com.domain.springboot.repositories.MovieRepository;
#Service
public class MovieImport {
private final MovieRepository movieRepository;
public MovieImport(MovieRepository movieRepository){
this.movieRepository = movieRepository;
}
public void saveToDb() {
// Create movie
com.domain.springboot.entities.Movie springbootMovie = new com.domain.springboot.entities.Movie("Iron Man", "284cb8fgf");
this.movieRepository.save(springbootMovie);
}
}
Updated
#SpringBootApplication
public class InfMApplication implements CommandLineRunner {
#Autowired
private MovieImport movieImport;
public static void main(String[] args) {
SpringApplication.run(InfMApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
movieImport.saveToDb();
}
}

Error with StreamIdentifier when using MultiStreamTracker in kinesis

I'm getting an error with StreamIdentifier when trying to use MultiStreamTracker in a kinesis consumer application.
java.lang.IllegalArgumentException: Unable to deserialize StreamIdentifier from first-stream-name
What is causing this error? I can't find a good example of using the tracker with kinesis.
The stream name works when using a consumer with a single stream so I'm not sure what is happening. It looks like the consumer is trying to parse the accountId and streamCreationEpoch. But when I create the identifiers I am using the singleStreamInstance method. Is the stream name required to have these values? They appear to be optional from the code.
This test is part of a complete example on github.
package kinesis.localstack.example;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.amazonaws.services.kinesis.producer.KinesisProducer;
import com.amazonaws.services.kinesis.producer.KinesisProducerConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.kinesis.KinesisAsyncClient;
import software.amazon.kinesis.common.ConfigsBuilder;
import software.amazon.kinesis.common.InitialPositionInStream;
import software.amazon.kinesis.common.InitialPositionInStreamExtended;
import software.amazon.kinesis.common.KinesisClientUtil;
import software.amazon.kinesis.common.StreamConfig;
import software.amazon.kinesis.common.StreamIdentifier;
import software.amazon.kinesis.coordinator.Scheduler;
import software.amazon.kinesis.exceptions.InvalidStateException;
import software.amazon.kinesis.exceptions.ShutdownException;
import software.amazon.kinesis.lifecycle.events.InitializationInput;
import software.amazon.kinesis.lifecycle.events.LeaseLostInput;
import software.amazon.kinesis.lifecycle.events.ProcessRecordsInput;
import software.amazon.kinesis.lifecycle.events.ShardEndedInput;
import software.amazon.kinesis.lifecycle.events.ShutdownRequestedInput;
import software.amazon.kinesis.processor.FormerStreamsLeasesDeletionStrategy;
import software.amazon.kinesis.processor.FormerStreamsLeasesDeletionStrategy.NoLeaseDeletionStrategy;
import software.amazon.kinesis.processor.MultiStreamTracker;
import software.amazon.kinesis.processor.ShardRecordProcessor;
import software.amazon.kinesis.processor.ShardRecordProcessorFactory;
import software.amazon.kinesis.retrieval.KinesisClientRecord;
import software.amazon.kinesis.retrieval.polling.PollingConfig;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.CLOUDWATCH;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.DYNAMODB;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.KINESIS;
import static software.amazon.kinesis.common.InitialPositionInStream.TRIM_HORIZON;
import static software.amazon.kinesis.common.StreamIdentifier.singleStreamInstance;
#Testcontainers
public class KinesisMultiStreamTest {
static class TestProcessorFactory implements ShardRecordProcessorFactory {
private final TestKinesisRecordService service;
public TestProcessorFactory(TestKinesisRecordService service) {
this.service = service;
}
#Override
public ShardRecordProcessor shardRecordProcessor() {
throw new UnsupportedOperationException("must have streamIdentifier");
}
public ShardRecordProcessor shardRecordProcessor(StreamIdentifier streamIdentifier) {
return new TestRecordProcessor(service, streamIdentifier);
}
}
static class TestRecordProcessor implements ShardRecordProcessor {
public final TestKinesisRecordService service;
public final StreamIdentifier streamIdentifier;
public TestRecordProcessor(TestKinesisRecordService service, StreamIdentifier streamIdentifier) {
this.service = service;
this.streamIdentifier = streamIdentifier;
}
#Override
public void initialize(InitializationInput initializationInput) {
}
#Override
public void processRecords(ProcessRecordsInput processRecordsInput) {
service.addRecord(streamIdentifier, processRecordsInput);
}
#Override
public void leaseLost(LeaseLostInput leaseLostInput) {
}
#Override
public void shardEnded(ShardEndedInput shardEndedInput) {
try {
shardEndedInput.checkpointer().checkpoint();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
#Override
public void shutdownRequested(ShutdownRequestedInput shutdownRequestedInput) {
}
}
static class TestKinesisRecordService {
private List<ProcessRecordsInput> firstStreamRecords = Collections.synchronizedList(new ArrayList<>());
private List<ProcessRecordsInput> secondStreamRecords = Collections.synchronizedList(new ArrayList<>());
public void addRecord(StreamIdentifier streamIdentifier, ProcessRecordsInput processRecordsInput) {
if(streamIdentifier.streamName().contains(firstStreamName)) {
firstStreamRecords.add(processRecordsInput);
} else if(streamIdentifier.streamName().contains(secondStreamName)) {
secondStreamRecords.add(processRecordsInput);
} else {
throw new IllegalStateException("no list for stream " + streamIdentifier);
}
}
public List<ProcessRecordsInput> getFirstStreamRecords() {
return Collections.unmodifiableList(firstStreamRecords);
}
public List<ProcessRecordsInput> getSecondStreamRecords() {
return Collections.unmodifiableList(secondStreamRecords);
}
}
public static final String firstStreamName = "first-stream-name";
public static final String secondStreamName = "second-stream-name";
public static final String partitionKey = "partition-key";
DockerImageName localstackImage = DockerImageName.parse("localstack/localstack:latest");
#Container
public LocalStackContainer localstack = new LocalStackContainer(localstackImage)
.withServices(KINESIS, CLOUDWATCH)
.withEnv("KINESIS_INITIALIZE_STREAMS", firstStreamName + ":1," + secondStreamName + ":1");
public Scheduler scheduler;
public TestKinesisRecordService service = new TestKinesisRecordService();
public KinesisProducer producer;
#BeforeEach
void setup() {
KinesisAsyncClient kinesisClient = KinesisClientUtil.createKinesisAsyncClient(
KinesisAsyncClient.builder().endpointOverride(localstack.getEndpointOverride(KINESIS)).region(Region.of(localstack.getRegion()))
);
DynamoDbAsyncClient dynamoClient = DynamoDbAsyncClient.builder().region(Region.of(localstack.getRegion())).endpointOverride(localstack.getEndpointOverride(DYNAMODB)).build();
CloudWatchAsyncClient cloudWatchClient = CloudWatchAsyncClient.builder().region(Region.of(localstack.getRegion())).endpointOverride(localstack.getEndpointOverride(CLOUDWATCH)).build();
MultiStreamTracker tracker = new MultiStreamTracker() {
private List<StreamConfig> configs = List.of(
new StreamConfig(singleStreamInstance(firstStreamName), InitialPositionInStreamExtended.newInitialPosition(TRIM_HORIZON)),
new StreamConfig(singleStreamInstance(secondStreamName), InitialPositionInStreamExtended.newInitialPosition(TRIM_HORIZON)));
#Override
public List<StreamConfig> streamConfigList() {
return configs;
}
#Override
public FormerStreamsLeasesDeletionStrategy formerStreamsLeasesDeletionStrategy() {
return new NoLeaseDeletionStrategy();
}
};
ConfigsBuilder configsBuilder = new ConfigsBuilder(tracker, "KinesisPratTest", kinesisClient, dynamoClient, cloudWatchClient, UUID.randomUUID().toString(), new TestProcessorFactory(service));
scheduler = new Scheduler(
configsBuilder.checkpointConfig(),
configsBuilder.coordinatorConfig(),
configsBuilder.leaseManagementConfig(),
configsBuilder.lifecycleConfig(),
configsBuilder.metricsConfig(),
configsBuilder.processorConfig().callProcessRecordsEvenForEmptyRecordList(false),
configsBuilder.retrievalConfig()
);
new Thread(scheduler).start();
producer = producer();
}
#AfterEach
public void teardown() throws ExecutionException, InterruptedException, TimeoutException {
producer.destroy();
Future<Boolean> gracefulShutdownFuture = scheduler.startGracefulShutdown();
gracefulShutdownFuture.get(60, TimeUnit.SECONDS);
}
public KinesisProducer producer() {
var configuration = new KinesisProducerConfiguration()
.setVerifyCertificate(false)
.setCredentialsProvider(localstack.getDefaultCredentialsProvider())
.setMetricsCredentialsProvider(localstack.getDefaultCredentialsProvider())
.setRegion(localstack.getRegion())
.setCloudwatchEndpoint(localstack.getEndpointOverride(CLOUDWATCH).getHost())
.setCloudwatchPort(localstack.getEndpointOverride(CLOUDWATCH).getPort())
.setKinesisEndpoint(localstack.getEndpointOverride(KINESIS).getHost())
.setKinesisPort(localstack.getEndpointOverride(KINESIS).getPort());
return new KinesisProducer(configuration);
}
#Test
void testFirstStream() {
String expected = "Hello";
producer.addUserRecord(firstStreamName, partitionKey, ByteBuffer.wrap(expected.getBytes(StandardCharsets.UTF_8)));
var result = await().timeout(600, TimeUnit.SECONDS)
.until(() -> service.getFirstStreamRecords().stream()
.flatMap(r -> r.records().stream())
.map(KinesisClientRecord::data)
.map(r -> StandardCharsets.UTF_8.decode(r).toString())
.collect(toList()), records -> records.size() > 0);
assertThat(result).anyMatch(r -> r.equals(expected));
}
#Test
void testSecondStream() {
String expected = "Hello";
producer.addUserRecord(secondStreamName, partitionKey, ByteBuffer.wrap(expected.getBytes(StandardCharsets.UTF_8)));
var result = await().timeout(600, TimeUnit.SECONDS)
.until(() -> service.getSecondStreamRecords().stream()
.flatMap(r -> r.records().stream())
.map(KinesisClientRecord::data)
.map(r -> StandardCharsets.UTF_8.decode(r).toString())
.collect(toList()), records -> records.size() > 0);
assertThat(result).anyMatch(r -> r.equals(expected));
}
}
Here is the error I am getting.
[Thread-9] ERROR software.amazon.kinesis.coordinator.Scheduler - Worker.run caught exception, sleeping for 1000 milli seconds!
java.lang.IllegalArgumentException: Unable to deserialize StreamIdentifier from first-stream-name
at software.amazon.kinesis.common.StreamIdentifier.multiStreamInstance(StreamIdentifier.java:75)
at software.amazon.kinesis.coordinator.Scheduler.getStreamIdentifier(Scheduler.java:1001)
at software.amazon.kinesis.coordinator.Scheduler.buildConsumer(Scheduler.java:917)
at software.amazon.kinesis.coordinator.Scheduler.createOrGetShardConsumer(Scheduler.java:899)
at software.amazon.kinesis.coordinator.Scheduler.runProcessLoop(Scheduler.java:419)
at software.amazon.kinesis.coordinator.Scheduler.run(Scheduler.java:330)
at java.base/java.lang.Thread.run(Thread.java:829)
According to documentation:
The serialized stream identifier should be of the following format: account-id:StreamName:streamCreationTimestamp
So your code should be like this:
private List<StreamConfig> configs = List.of(
new StreamConfig(multiStreamInstance("111111111:multiStreamTest-1:12345"), InitialPositionInStreamExtended.newInitialPosition(TRIM_HORIZON)),
new StreamConfig(multiStreamInstance("111111111:multiStreamTest-2:12389"), InitialPositionInStreamExtended.newInitialPosition(TRIM_HORIZON)));
Note: this also will change leaseKey format to account-id:StreamName:streamCreationTimestamp:ShardId

Why can I not access this method from another class?

I have an Android app where I'm trying to make a GET request to my API using Retrofit2. I have copied some code online that allows me to add the access token to the body of the request as this is how my API is setup. Following the code online, the author is able to call the api variable from a different class but I am unable to.
Here is the class the api variable is defined:
cCustomerService.java
package com.example.dentdevils.helper.retrofit.oauth2.client;
import android.content.Context;
import android.content.SharedPreferences;
import com.example.dentdevils.helper.retrofit.oauth2.OauthConstant;
import com.example.dentdevils.helper.retrofit.oauth2.service.CustomerService;
import java.io.IOException;
import okhttp3.FormBody;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.Buffer;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;
import static com.example.dentdevils.MainActivity.access;
import static com.example.dentdevils.MainActivity.mypreference;
public class cCustomerService {
public final CustomerService api;
private Context mContext;
SharedPreferences sharedPreferences;
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder requestBuilder = request.newBuilder();
mContext.getSharedPreferences(mypreference, Context.MODE_PRIVATE);
RequestBody formBody = new FormBody.Builder()
.add("access_token", sharedPreferences.getString(access, ""))
.build();
String postBodyToString = bodyToString(request.body());
postBodyToString += ((postBodyToString.length() > 0) ? "&" : "") + bodyToString(formBody);
request = requestBuilder.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"), postBodyToString))
.build();
return chain.proceed(request);
}
}).build();
public static String bodyToString(final RequestBody requestBody) {
try {
final RequestBody copy = requestBody;
final Buffer buffer = new Buffer();
if (copy != null) {
copy.writeTo(buffer);
} else {
return "";
}
return buffer.readUtf8();
} catch (IOException e) {
return "didn't work";
}
}
public cCustomerService() {
Retrofit retrofit = new Retrofit.Builder()
.client(client)
.baseUrl(OauthConstant.DATA_SERVER_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
api = retrofit.create(CustomerService.class);
}
}
As you can see I set the api variables value at the bottom.
Now I want to access it in the following class:
ViewCustomers.Java
package com.example.dentdevils;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import com.example.dentdevils.helper.retrofit.oauth2.OauthConstant;
import com.example.dentdevils.helper.retrofit.oauth2.client.cCustomerService;
import com.example.dentdevils.helper.retrofit.oauth2.response.CustomerResponse;
import com.example.dentdevils.helper.retrofit.oauth2.service.CustomerService;
import java.io.IOException;
import java.util.List;
import okhttp3.FormBody;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import static com.example.dentdevils.MainActivity.access;
import static com.example.dentdevils.MainActivity.mypreference;
import static com.example.dentdevils.helper.retrofit.oauth2.client.cCustomerService.bodyToString;
public class ViewCustomers extends AppCompatActivity {
private Context mContext;
SharedPreferences sharedPreferences;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_customers);
CustomerService customerService = api.create(CustomerService.class);
Call<List<CustomerResponse>> callArray = customerService.getCustomers();
callArray.enqueue(new Callback<List<CustomerResponse>>() {
#RequiresApi(api = Build.VERSION_CODES.N)
#Override
public void onResponse(Call<List<CustomerResponse>> call, retrofit2.Response<List<CustomerResponse>> response) {
if (response.isSuccessful())
{
List<CustomerResponse> customers = response.body();
customers.forEach(customer -> System.out.println(customer.getName()));
} else {
Log.e("TAG", "Failed!");
}
}
#Override
public void onFailure(Call<List<CustomerResponse>> call, Throwable t) {
Log.e("TAG", t.getLocalizedMessage());
}
});
}
}
As you can see I reference the api variable on this line CustomerService customerService = api.create(CustomerService.class); but I get an error saying it cannot resolve symbol 'api'. Like I said the author of the post has his setup exactly like mine so why isn't mine working? I added the relevant imports and I still can't access it. Any help would be appreciated.

How to get the email-verification link inside my custom SPI in keycloak

I have my code below, this is inside my notification-spi project, which get triggered when a new user is created. I am able to receive the email. However i don't know how i can get the email-verification link when RequiredActions verify-email is selected by the admin who created the account in keycloak admin ui.
public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) {
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
RealmModel realm = session.realms().getRealm(adminEvent.getRealmId());
UserModel user = session.userCache().getUserById(adminEvent.getAuthDetails().getUserId(),
realm);
if (OperationType.CREATE.equals(adminEvent.getOperationType())) {
LOGGER.info("OPERATION CREATE USER");
LOGGER.info("Representation : " + adminEvent.getRepresentation());
try {
LOGGER.info("Sending email...");
emailSender.send(realm.getSmtpConfig(), user, "Account Enrollment",
"A new account has been created using your email.",
"<h1>Account Enrollment</h1> <br/>"
+ "<p>A new account has been created using your email</p>");
LOGGER.info("Email has been sent.");
} catch (EmailException e) {
LOGGER.info(e.getMessage());
}
}
}
}
Any help is appreciated.
You probably don't need this anymore, but this thread was the first result when I searched for how to get verification link in SPI. I guess other people can find it useful. After some time i came up with smth like this:
import org.jboss.logging.Logger;
import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken;
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.Urls;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.*;
import java.util.stream.Collectors;
public class CustomResourceProvider implements RealmResourceProvider {
private static final Logger log = Logger.getLogger(CustomResourceProvider.class);
private final KeycloakSession session;
public CustomResourceProvider(KeycloakSession session) {
this.session = session;
}
#GET
#Path("users/test2")
#Produces({MediaType.APPLICATION_JSON})
public Response sendVerificationLink() throws EmailException {
RealmModel realm = session.getContext().getRealm();
final Map<String, String> searchParams = new HashMap<String, String>() {{
put("emailVerified", "false");
}};
final List<UserModel> users = session
.users()
.searchForUserStream(realm, searchParams)
.collect(Collectors.toList());
EmailTemplateProvider emailTemplateProvider = session.getProvider(EmailTemplateProvider.class);
for (UserModel user : users) {
int expiration = Time.currentTime() + 1000000;
VerifyEmailActionToken token = new VerifyEmailActionToken(
user.getId(),
expiration,
"oasid",
user.getEmail(),
"localdev"
);
UriBuilder builder = LoginActionsService.actionTokenProcessor(session.getContext().getUri());
builder.queryParam("key", token.serialize(session, realm, session.getContext().getUri()));
String verificationLink = builder.build(realm.getName()).toString();
emailTemplateProvider
.setRealm(realm)
.setUser(user)
.sendVerifyEmail(verificationLink, 100000);
}
return Response
.status(Response.Status.OK)
.entity(users.size())
.build();
}
#Override
public Object getResource() {
return this;
}
#Override
public void close() {
}
}

Java : Android : Retrofit 2 response code is 200, response.body is null

I am trying to build an Android App that implements a Collaborative Filtering Algorithm using Retrofit 2,Realm and The Movie Database API.
When making my Retrofit callback, onResponse returns a successful status code (200), but from logging I get that my response.body().getResults returns null. I'm in this pickle now and I can't get it to work.My ApiService seem to be working fine and I make other retrofit callbacks to get directors,movies by title,movies by release date.Anyways, here is some code snippets that might be helpful.
APIService.java
package com.yannis.thesis.movierecommendationapp.api;
import com.yannis.thesis.movierecommendationapp.models.DirectorResponse;
import com.yannis.thesis.movierecommendationapp.models.GenreResponse;
import com.yannis.thesis.movierecommendationapp.models.Movie;
import com.yannis.thesis.movierecommendationapp.models.MovieResponse;
import com.yannis.thesis.movierecommendationapp.models.PrimaryMovieInfo;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface APIService {
#POST("/list")
Call<Movie> loadMovie();
#GET("movie/top_rated")
Call<MovieResponse> getTopRatedMovies(#Query("api_key") String apiKey);
#GET("movie/popular")
Call<MovieResponse> getPopularMovies(#Query("api_key") String apiKey);
#GET("movie/{id}")
Call<MovieResponse> getMovieDetails(#Path("id") int id, #Query("api_key") String apiKey);
#GET("search/movie")
Call<MovieResponse> getMovieByTitle(#Query("query") String title, #Query("api_key") String apiKey);
}
MovieResponse.java
package com.yannis.thesis.movierecommendationapp.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class MovieResponse {
#SerializedName("page")
private Integer page;
#SerializedName("results")
private List<Movie> results;
#SerializedName("total_results")
private Integer totalResults;
#SerializedName("total_pages")
private Integer totalPages;
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public List<Movie> getResults() {
return results;
}
public void setResults(List<Movie> results) {
this.results = results;
}
public Integer getTotalResults() {
return totalResults;
}
public void setTotalResults(Integer totalResults) {
this.totalResults = totalResults;
}
public Integer getTotalPages() {
return totalPages;
}
public void setTotalPages(Integer totalPages) {
this.totalPages = totalPages;
}
}
MovieRecommendationApp.java
package com.yannis.thesis.movierecommendationapp;
import android.app.Application;
import android.support.annotation.NonNull;
import android.support.v4.view.PagerAdapter;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import com.luseen.logger.LogType;
import com.luseen.logger.Logger;
import com.weiwangcn.betterspinner.library.BetterSpinner;
import com.yannis.thesis.movierecommendationapp.activities.BaseActivity;
import com.yannis.thesis.movierecommendationapp.activities.MainActivity;
import com.yannis.thesis.movierecommendationapp.api.APIService;
import com.yannis.thesis.movierecommendationapp.models.DirectorResponse;
import com.yannis.thesis.movierecommendationapp.models.DirectorResult;
import com.yannis.thesis.movierecommendationapp.models.Genre;
import com.yannis.thesis.movierecommendationapp.models.GenreResponse;
import com.yannis.thesis.movierecommendationapp.models.MainPagerEnum;
import com.yannis.thesis.movierecommendationapp.models.Movie;
import com.yannis.thesis.movierecommendationapp.models.MovieResponse;
import com.yannis.thesis.movierecommendationapp.models.MovieRecommendedForUser;
import com.yannis.thesis.movierecommendationapp.models.Recommendation;
import com.yannis.thesis.movierecommendationapp.models.User;
import com.yannis.thesis.movierecommendationapp.models.UserRatesMovie;
import com.yannis.thesis.movierecommendationapp.MovieRecommendationApp;
import com.yannis.thesis.movierecommendationapp.R;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.RealmQuery;
import io.realm.RealmResults;
import io.realm.Sort;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MovieRecommendationApp extends Application {
private static MovieRecommendationApp instance;
public BaseActivity lastActivity;
String API_BASE_URL = "http://api.themoviedb.org/3/";
private final static String API_KEY = "******************";
private static Retrofit retrofitinstance;
private String loggedInUserId;
private Realm realm;
final Double SIMILARITY_PILLOW = 0.5;
final Double PREDICTION_PILLOW = 3.0;
private APIService client;
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(
GsonConverterFactory.create()
);
Retrofit retrofit =
builder
.client(
httpClient.build()
)
.build();
retrofit2.Call<MovieResponse> call;
#Override
public void onCreate() {
super.onCreate();
instance = this;
Realm.init(this);
RealmConfiguration config = new RealmConfiguration.Builder()
.name("myrealmDB.realm")
.deleteRealmIfMigrationNeeded()
.build();
Realm.setDefaultConfiguration(config);
new Logger.Builder()
.isLoggable(BuildConfig.DEBUG)
.logType(LogType.WARN)
.tag("Iamerror")
.build();
realm = Realm.getDefaultInstance();
MovieRecommendationAlgorithm();
}
public static MovieRecommendationApp getInstance() {
return instance;
}
public static Retrofit getRetrofitInstance() {
return retrofitinstance;
}
public static String getApiKey() {
return API_KEY;
}
public void MovieRecommendationAlgorithm() {
prediction("activeUserId","637",neighbours);
}
public void prediction(String activeUserId, String notYetRatedMovieId,ArrayList<String> neightbours) {
Double activeAVG = avgRating(activeUserId);
Double A = 0.0;
Double B = 0.0;
for (int i = 0; i < neightbours.size(); i++) {
avgRating(neightbours.get(i));
A = A + similarity(activeUserId, neightbours.get(i)) * (getUser_i_MovieRating(neightbours.get(i), notYetRatedMovieId) - avgRating(neightbours.get(i)));
B = B + similarity(activeUserId, neightbours.get(i));
}
final Double prediction = activeAVG + A / B;
if (prediction < PREDICTION_PILLOW) {
return;
}
int movieId = Integer.parseInt(notYetRatedMovieId);
client = retrofit.create(APIService.class);
call = client.getMovieDetails(movieId, MovieRecommendationApp.getApiKey());
call.enqueue(new retrofit2.Callback<MovieResponse>() {
#Override
public void onResponse(retrofit2.Call<MovieResponse> call, retrofit2.Response<MovieResponse> response) {
int statusCode = response.code();
if (response.isSuccessful() == false) {
Logger.w("unsuccessful w status", String.valueOf(statusCode));
} else {
//problem is in this spot
Logger.w( " reponse body is " + response.body().getResults());
}
// Logger.e("Number of movies received: " + movies.size());
}
#Override
public void onFailure(retrofit2.Call<MovieResponse> call, Throwable t) {
}
});
}
}
I double checked the API call using Postman - calling https://api.themoviedb.org/3/movie/637?api_key=*********&language=en-US
got me the desired JSON Body.
Thank you for you time and help.
I don't know if this will be helpfull, but here: if (response.isSuccessful() == false)
It's a quite strange compare a boolean method isSuccessful() with false, the method already return a boolean.
After searching at the movie database api forum , I found this [https://www.themoviedb.org/talk/5667650ec3a36836970002bc][1]
in which it is stated that "The only way we currently support is via query parameters." and there is a big possibility for my problem to be related with the fact that I am using a #Path parameter and not a #Query parameter.
So now the million dollar question is how to convert the
#GET("movie/{id}")
Call<MovieResponse> getMovieDetails(#Path("id") int id, #Query("api_key") String apiKey);
to a Call using the #Query annotation.

Categories