I have a HAL API that I'm working with and in many cases I need to send request (with different methods) to a URL that I get back from API. Meaning I don't want to hardcode the path of URL in my retrofit api interface but what I want is just sending a simple request using retrofit to that URL.
I'm using Volley now and I know that I can use OkHttp for this purpose but I was wondering if there is a nice way of doing such thing in Retrofit?
Recently Square has released the Retrofit v2.0.0 BETA and it has a built-in support for dynamic URLs. Even though the Library is in Beta, based on what Jake Wharton told us in DroidCon NYC 2015, all the apis are stable and will not change. I'm personally adding it to my production so its up to you.
You will find the following links useful if you decide to do the upgrade:
Jake Wharton presentation # DroidCon NYC 2015
A very good guide on the changes
In simple word, you can now use the api annotations (like #GET or #POST and others) without any path and then you pass in a #URL to your api method that the method will use to call.
----------------Retrofit 1.x
I figured out a nice way for doing this and would like to share it.
The trick is to use the dynamic URL as your End Point in the creation of RestAdapter and then have a empty path on your API interface.
Here is how I did it:
public RestAdapter getHostAdapter(String baseHost){
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(baseHost)
.setRequestInterceptor(requestInterceptor)
.build();
return restAdapter;
}
I build my restAdapter using this method and then I have this in my interface: (this will not work if your URL has query parameters added to it. See next answer for solution to that case)
public interface General {
#GET("/")
void getSomething(Callback<SomeObject> callback);
}
and finally using them like this:
getHostAdapter("YOUR_DYNAMIC_URL").create(General.class)
.getSomething(new Callback<SomeObject>(){
...
})
Hope it helps.
In case that your URL has query parameters on it, the above solution will not work since it will add the '/' at the end of your base URL. for example if your URL is
https://www.google.com/?q=test
then the above solution will try to send the request to
https://www.google.com/?q=test/
which will fail because of mall format.
What we can do is one extra step and parsing the url. By parsing I mean just taking out all URL parameters and sending them in a QueryMap.
Here is how:
We should have the same structure describe above with a little change to our interface
public interface General {
#GET("/")
void getSomething(#QueryMap Map<String,String> queryMap, Callback<SomeObject> callback);
}
I just added a QueryMap to the above interface and now we can use this parser method:
public static void getSomething(#NonNull String urlString, #NonNull Callback<SomeObject> callback){
Uri uri = Uri.parse(urlString);
Set<String> queryParameterNames = uri.getQueryParameterNames();
String host = uri.getHost();
HashMap<String,String> queryMap = new HashMap<>();
Iterator<String> iterator = queryParameterNames.iterator();
while(iterator.hasNext()){
String queryName = iterator.next();
String queryParameter = uri.getQueryParameter(queryName);
queryMap.put(queryName, queryParameter);
}
getHostAdapter(host)
.create(General.class)
.getSomething(queryMap, callback);
}
now you can call this method like this:
getSomething("https://www.google.com/?q=test");
Enjoy coding.
Note: QueryMap was added on Retrofit v1.4.0
I also need a path on my URL, so I did this:
#GET("/{path}")
void getMatcherUrl(#Path(value = "path", encode = false) String path, #QueryMap Map<String, String> queryMap, RestCallback<MatcherResponse> matcherResponse);
/**
* Need to create a custom method because i need to pass a absolute url to the retrofit client
*
* #param urlString
* #param matcherResponse
*/
public void getMatcherUrl(#NonNull String urlString, #NonNull RestCallback<MatcherResponse> matcherResponse) {
Uri uri = Uri.parse(urlString);
Set<String> queryParameterNames = uri.getQueryParameterNames();
String host = uri.getHost();
String path = (uri.getPath().startsWith("/")) ? uri.getPath().substring(1) : uri.getPath();
HashMap<String, String> queryMap = new HashMap<>();
Iterator<String> iterator = queryParameterNames.iterator();
while (iterator.hasNext()) {
String queryName = iterator.next();
String queryParameter = uri.getQueryParameter(queryName);
queryMap.put(queryName, queryParameter);
}
getApiCoreService(host)
.getMatcherUrl(path, queryMap, matcherResponse);
}
public ApiCoreService getApiCoreService(String host) {
if (StringUtils.isEmpty(host))
this.endpoint = new RestEndpoint(RemoteConfigurationManager.getInstance().getApiCore(), "ApiCore");
else
this.endpoint = new RestEndpoint(host, "ApiCore");
return apiCoreService;
}
Adding to above two answers, Here is a working class that makes use of Queryparam and fires the absolute URL
public class VideoClient {
private static final String TAG = "VideoCLient";
private final RestAdapter restAdapter;
private General apiService;
private String hostName;
private LinkedHashMap<String, String> queryMap;
private String Url_Path;
public VideoClient(String BASE_URL) {
Log.d(TAG,"Base url is "+BASE_URL);
hostName =getHostNameAndGenerateQueryMap(BASE_URL);
Gson gson = new GsonBuilder()
.create();
RequestInterceptor interceptor = new RequestInterceptor() {
#Override
public void intercept(RequestFacade request) {}
};
restAdapter = new RestAdapter.Builder()
.setLogLevel(RestAdapter.LogLevel.FULL)
.setEndpoint("http://"+hostName)
.setClient(new OkClient())
.setConverter(new GsonConverter(gson))
.setRequestInterceptor(interceptor)
.build();
private String getHostNameAndGenerateQueryMap(String urlString) {
Uri uri = Uri.parse(urlString);
Url_Path = (uri.getPath().startsWith("/")) ? uri.getPath().substring(1) : uri.getPath();
Set<String> queryParameterNames = uri.getQueryParameterNames();
String host = uri.getHost();
queryMap = new LinkedHashMap<>();
Iterator<String> iterator = queryParameterNames.iterator();
while (iterator.hasNext()) {
String queryName = iterator.next();
String queryParameter = uri.getQueryParameter(queryName);
Log.d(TAG,"query name "+queryName +" query param "+queryParameter);
queryMap.put(queryName, queryParameter);
}
return host;
}
public interface General {
/*void getVideo(#Path("auth_token") String authId,
#Query("et") String systemTime,#Query("service_id") String serviceID,
#Query("protocol") String scheme,#Query("play_url") String url,
#Query("us") String us,Callback<String> callback);
*/
#GET("/{path}")
getVideo(#Path(value="path", encode=false)String path,#QueryMap LinkedHashMap<String, String> queryMap);
}
public void getVideoDetails() {
Log.i("firing", "getVideoApi");
Log.d(TAG, "firing " + Url_Path + " function");
restAdapter.create(General.class).getVideo(Url_Path,queryMap, new Callback<Object>() {
#Override
public void success( Object o, Response response) {
Log.d(TAG," Success Response is "+response );
}
#Override
public void failure(RetrofitError error) {
Log.d(TAG, "Failure " + "Internal Error" + error);
}
});
}
Related
I'm attempting to use Retrofit to call the GitHub API to update the contents of an existing file, but am getting 404s in my responses. For this question, I'm interested in updating this file. Here is the main code I wrote to try and achieve this:
GitHubUpdateFileRequest
public class GitHubUpdateFileRequest {
public String message = "Some commit message";
public String content = "Hello World!!";
public String sha = "shaRetrievedFromSuccessfulGETOperation";
public final Committer committer = new Committer();
private class Committer {
Author author = new Author();
private class Author {
final String name = "blakewilliams1";
final String email = "blake#blakewilliams.org";
}
}
}
**GitHubUpdateFileResponse **
public class GitHubUpdateFileResponse {
public GitHubUpdateFileResponse() {}
}
GitHubClient
public interface GitHubClient {
// Docs: https://docs.github.com/en/rest/reference/repos#get-repository-content
// WORKS FINE
#GET("/repos/blakewilliams1/blakewilliams1.github.io/contents/qr_config.json")
Call<GitHubFile> getConfigFile();
// https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents
// DOES NOT WORK
#PUT("/repos/blakewilliams1/blakewilliams1.github.io/contents/qr_config.json")
Call<GitHubUpdateFileResponse> updateConfigFile(#Body GitHubUpdateFileRequest request);
}
Main Logic
// Set up the Retrofit client and add an authorization interceptor
UserAuthInterceptor interceptor =
new UserAuthInterceptor("blake#blakewilliams.org", "myActualGitHubPassword");
OkHttpClient.Builder httpClient =
new OkHttpClient.Builder().addInterceptor(interceptor);
Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create());
Retrofit retrofit = builder.client(httpClient.build()).build();
client = retrofit.create(GitHubClient.class);
// Now make the request and process the response
GitHubUpdateFileRequest request = new GitHubUpdateFileRequest();
client.updateConfigFile(request).enqueue(new Callback<GitHubUpdateFileResponse>() {
#Override
public void onResponse(Call<GitHubUpdateFileResponse> call, Response<GitHubUpdateFileResponse> response) {
int responseCode = response.code();
// More code on successful update
}
#Override
public void onFailure(Call<GitHubUpdateFileResponse> call, Throwable t) {
Log.e("MainActivity", "Unable to update file" + t.getLocalizedMessage());
}
});
What currently happens:
Currently, the success callback is triggered, but with a response code of 404 like so:
Response{protocol=http/1.1, code=404, message=Not Found, url=https://api.github.com/repos/blakewilliams1/blakewilliams1.github.io/contents/qr_config.json}
Has anyone else encountered this? I first thought it was a problem with including '/content/' in the URL but I do the same thing for reading the file contents request and it works fine (also uses same URL just a GET instead of PUT).
For anyone interested in doing this in the future, I figured out the solution.
I needed to revise the request object structure
Rather than using an authentication interceptor, I instead added an access token to the header. Here is where you can create access tokens for Github, you only need to grant it permissions to the 'repos' options for this use case to work.
This is what my updated request object looks like:
public class GitHubUpdateFileRequest {
public String message;
public String content;
public String sha;
public final Committer committer = new Committer();
public GitHubUpdateFileRequest(String unencodedContent, String message, String sha) {
this.message = message;
this.content = Base64.getEncoder().encodeToString(unencodedContent.getBytes());
this.sha = sha;
}
private static class Committer {
final String name = "yourGithubUsername";
final String email = "email#yourEmailAddressForTheUsername.com";
}
}
Then from my code, I would just say:
GitHubUpdateFileRequest updateRequest = new GitHubUpdateFileRequest("Hello World File Contents", "This is the title of the commit", shaOfExistingFile);
For using this reqest, I updated the Retrofit client implementation like so:
// https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents
#Headers({"Content-Type: application/vnd.github.v3+json"})
#PUT("/repos/yourUserName/yourRepository/subfolder/path/to/specific/file/theFile.txt")
Call<GitHubUpdateFileResponse> updateConfigFile(
#Header("Authorization") String authorization, #Body GitHubUpdateFileRequest request);
And I call that interface like this:
githubClient.updateConfigFile("token yourGeneratedGithubToken", request);
And yes, you do need the prefix "token ". You could hardcode that header into the interface, but I pass it in so that I can store it in locations outside of my version control's reach for security reasons.
So far (up to Play 2.3) to create a mock request I used Helpers.fakeRequest().withAnyContent(). E.g.
private Request getMultiPartFormDataRequestForFileUpload(File file,
String filePartKey, String contentType) {
FilePart<TemporaryFile> part = new MultipartFormData.FilePart<>(
filePartKey, file.getName(), Scala.Option(contentType),
new TemporaryFile(file));
List<FilePart<TemporaryFile>> fileParts = new ArrayList<>();
fileParts.add(part);
scala.collection.immutable.List<FilePart<TemporaryFile>> files = scala.collection.JavaConversions
.asScalaBuffer(fileParts).toList();
MultipartFormData<TemporaryFile> formData = new MultipartFormData<TemporaryFile>(
null, files, null, null);
return Helpers.fakeRequest().withAnyContent(formData);
}
In Play 2.4 this isn't possible any more. If I look at the source of RequestBuilder (which is implemented by FakeRequest) a similar function exist, but it's protected and I can't use it.
protected Http.RequestBuilder body(play.api.mvc.AnyContent anyContent)
Set a AnyContent to this request.
Does anyone know how I can write a test case to check a file upload with MultipartFormData in 2.4?
As Helpers.fakeRequest is a very, very simple method (taken from github)
/**
* Build a new GET / fake request.
*/
public static RequestBuilder fakeRequest() {
return fakeRequest("GET", "/");
}
/**
* Build a new fake request.
*/
public static RequestBuilder fakeRequest(String method, String uri) {
return new RequestBuilder().method(method).uri(uri);
}
you can extend Http.RequestBuilder thus getting access to protected method:
public class FakeRequestBuilder extends HttpRequestBuilder() {
public RequestBuilder setAnyBody(RequestBody body, String contentType) {
header("Content-Type", contentType);
body(body);
}
}
And use that in your test:
//OLD return Helpers.fakeRequest().withAnyContent(formData);
//NEW
return new FakeRequestBuilder().setAnyBody(formData, "multipart/form-data").build();
//or is it application/x-www-form-urlencoded for you?
In Play2.4, body() does not accept a RequestBody, and you have to create an AnyContent from multipart first. Example:
private class FakeRequestBuilder extends Http.RequestBuilder {
public FakeRequestBuilder(String method, String uri) {
method(method).uri(uri);
}
protected RequestBuilder setAnyBody(MultipartFormData<TemporaryFile> formData, String contentType) {
header("Content-Type", contentType);
AnyContent anyContent = new AnyContentAsMultipartFormData(formData);
body(anyContent);
return this;
}
}
A MultiPartFormData thingie can be created by this for example:
private MultipartFormData<TemporaryFile> createFormData(String contentType, String filePartKey, File file) {
FilePart<TemporaryFile> part = new MultipartFormData.FilePart<>(filePartKey, file.getName(), Scala.Option(contentType), new TemporaryFile(file));
List<FilePart<TemporaryFile>> fileParts = new ArrayList<>();
fileParts.add(part);
scala.collection.immutable.List<FilePart<TemporaryFile>> files = scala.collection.JavaConversions.asScalaBuffer(fileParts).toList();
MultipartFormData<TemporaryFile> formData = new MultipartFormData<TemporaryFile>(null, files, null, null);
return formData;
}
I'm trying to obtain a list of a user's tweets and I've run into some trouble when trying to authenticate my call to the API. I currently get a 401 when executing the code below:
public interface TwitterApi {
String API_URL = "https://api.twitter.com/1.1";
String CONSUMER_KEY = "<CONSUMER KEY GOES HERE>";
String CONSUMER_SECRET = "<CONSUMER SECRET GOES HERE>";
String ACCESS_TOKEN = "<ACCESS TOKEN GOES HERE>";
String ACCESS_TOKEN_SECRET = "<ACCESS TOKEN SECRET GOES HERE>";
#GET("/statuses/user_timeline.json")
List<Tweet> fetchUserTimeline(
#Query("count") final int count,
#Query("screen_name") final String screenName);
}
The following throws a 401 Authorisation error when calling fetchUserTimeline()
RetrofitHttpOAuthConsumer consumer = new RetrofitHttpOAuthConsumer(TwitterApi.CONSUMER_KEY, TwitterApi.CONSUMER_SECRET);
consumer.setTokenWithSecret(TwitterApi.ACCESS_TOKEN, TwitterApi.ACCESS_TOKEN_SECRET);
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(TwitterApi.API_URL)
.setClient(new SigningOkClient(consumer))
.build();
TwitterApi twitterApi = restAdapter.create(TwitterApi.class)
tweets = twitterApi.fetchUserTimeline(2, screenName);
I've also included the relevant code from the signpost-retrofit plugin:
public class SigningOkClient extends OkClient {
private final RetrofitHttpOAuthConsumer mOAuthConsumer;
public SigningOkClient(RetrofitHttpOAuthConsumer consumer) {
mOAuthConsumer = consumer;
}
public SigningOkClient(OkHttpClient client, RetrofitHttpOAuthConsumer consumer) {
super(client);
mOAuthConsumer = consumer;
}
#Override
public Response execute(Request request) throws IOException {
Request requestToSend = request;
try {
HttpRequestAdapter signedAdapter = (HttpRequestAdapter) mOAuthConsumer.sign(request);
requestToSend = (Request) signedAdapter.unwrap();
} catch (OAuthMessageSignerException | OAuthExpectationFailedException | OAuthCommunicationException e) {
// Fail to sign, ignore
e.printStackTrace();
}
return super.execute(requestToSend);
}
}
The signpost-retrofit plugin can be found here: https://github.com/pakerfeldt/signpost-retrofit
public class RetrofitHttpOAuthConsumer extends AbstractOAuthConsumer {
private static final long serialVersionUID = 1L;
public RetrofitHttpOAuthConsumer(String consumerKey, String consumerSecret) {
super(consumerKey, consumerSecret);
}
#Override
protected HttpRequest wrap(Object request) {
if (!(request instanceof retrofit.client.Request)) {
throw new IllegalArgumentException("This consumer expects requests of type " + retrofit.client.Request.class.getCanonicalName());
}
return new HttpRequestAdapter((Request) request);
}
}
Any help here would be great. The solution doesn't have to include the use of signpost but I do want to use Retrofit. I also do not want to show the user an 'Authenticate with Twitter' screen in a WebView - I simply want to display a handful of relevant tweets as part of a detail view.
Are you certain the signpost-retrofit project works for twitter oauth? I've used twitter4j successfully in the past - and if you don't want the full library you can use their code for reference. twitter4j
I have a POJO given below which I want to PUT to the server as JSON or XML.
This is what I have done
CLIENT:
ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);
WebTarget target = client.target(getBaseURI());
public void putFriend(String uri , Friend friend)
{
System.out.println(friend.toString());
target = target.path(some_path).path(uri);
ClientResponse response = target.request(MediaType.APPLICATION_JSON).put(Entity.entity(friend,MediaType.APPLICATION_JSON),ClientResponse.class);
}
Examples I found on web were using WebResource.
I don't know how to do using WebTarget. What I have done is taken from some example found on SO but Entity.entity() gives error undefined method entity(friend, String).
POJO
#XmlRootElement
public class Friend{
private String friendURI;
private String event;
private String uri;
String getUri() {
return uri;
}
void setUri(String uri) {
this.uri = uri;
}
String getFriendURI() {
return friendURI;
}
void setFriendURI(String friendURI) {
this.friendURI = friendURI;
}
String getEvent() {
return event;
}
void setEvent(String event) {
this.event = event;
}
public String toString() {
return "Friend [friendURI=" + friendURI + ", uri=" + uri + ", event=" + event
+ "]";
}
Please guide how to do this.
Thanks
There are two different Jersey major versions, 1.x and 2.x, You seems to be trying to use a combination of both, which won't work. The 2.x versions don't have some classes as in 1.x and vice versa.
If you want to use Jersey 2.x, then you should be using Response, rather than ClientResponse
Response response = target.request().put(Entity.json(friend));
// .json == automatic 'application/json'
See Working with the Client API for 2.x
Also as mentioned in your previous post, the getters and setters should be public for the Friend class
Also see the WebTarget API
Basic breakdown.
Calling request() on WebTarget returns an Invocation.Buidler
Invocation.Builder builder = target.request();
Once we call put, we get back a Response
Response response = builder.put(Entity.json(friend));
To extract a known type from the response, we could use readEntity(Class type)
String responseString = response.readEntity(String.class);
response.close();
I have a problem with passing Map parameters or object to Retrofit POST request.
I follow square, kdubb labs tutorials and this thread and I couldn't figure it out.
My current code which works:
public interface FacebookUser {
#FormUrlEncoded
#POST("/user/login-facebook")
void login(
#Field("fb_access_token") String fbAccessToken,
#Field("os") String os,
#Field("device") String device,
#Field("os_version") String osVersion,
#Field("app_version") String appVersion,
#Field("online") String online,
Callback<FacebookLoginUserResponse> callback
);
}
and code:
RestAdapter restAdapter = new RestAdapter.Builder()
.setServer(requestMaker.getUrl())
.build();
FacebookUser facebookUser = restAdapter.create(FacebookUser.class);
facebookUser.login(getFbAccessToken(),
getString(R.string.config_os),
Info.getAndroidId(getBaseContext()),
Build.VERSION.RELEASE,
Info.getAppVersionName(getBaseContext()),
"" + 1,
new Callback<FacebookLoginUserResponse>() {
#Override
public void success(FacebookLoginUserResponse facebookLoginUserResponse, Response response) {
}
#Override
public void failure(RetrofitError retrofitError) {
}
});
When I try to use this interface I receive from server that parameters are missing:
public interface FacebookUser {
#POST("/user/login-facebook")
void login(
#Body Map<String, String> map,
Callback<FacebookLoginUserResponse> callback
);
}
and map:
HashMap<String, String> map = new HashMap<String, String>();
map.put("fb_access_token", getFbAccessToken());
map.put("os", "android");
map.put("device", Info.getAndroidId(getBaseContext()));
map.put("os_version", Build.VERSION.RELEASE);
map.put("app_version", Info.getAppVersionName(getBaseContext()));
map.put("online", "" + 1);
Questions:
What is it wrong?
How can I pass object to request?
Well, now we can implement this thing (version 1.5.0).
#FormUrlEncoded
#POST("/oauth/access_token")
void getToken(
#FieldMap Map<String, String> params,
Callback<FacebookLoginUserResponse> callback
);
In retrofit 2.0 you have to do this way:
#FormUrlEncoded
#POST(Constant.API_Login)
Call<UserLoginPost> userLogin(#FieldMap Map<String, String> params);
This feature is still not supported by the Retrofit 1.2.2, however you can compile your own version from the master branch with this feature or wait for the next release.
https://github.com/square/retrofit/pull/390
Update:
It's available in Retrofit version 1.5.0 ! (ref Anton Golovin answer)