Retrofit and Slash characters in PATH - android

I am facing an issue with Retrofit and would like to find a suitable answer as the only way I can think of it is pretty ugly and not practical.
Retrofit PATH annotation requires a "/" in the beginning (as you can read in this code extracted from the library source:
/** Loads {#link #requestUrl}, {#link #requestUrlParamNames}, and {#link #requestQuery}. */
private void parsePath(String path) {
if (path == null || path.length() == 0 || path.charAt(0) != '/') {
throw methodError("URL path \"%s\" must start with '/'.", path);
}
The problem that I am facing is that the PATH part comes from the backend in a response object, meaning that all PATH's strings already come formatted from the backend previously in other response as follows:
Object : {
href: "/resources/login..."
}
As you can see, when including something like this, the URL gets malformed:
#GET("{/loginHref}")
void login(#EncodedPath("loginHref") String loginHref,
Callback<User> callback);
to something like "http://mybaseurl.com//resources/login" *double // in front of resources
This can definitely cause issues in some endpoints and I cannot think a really simple way to solve this issue apart from doing something like:
a) Modify my own version of retrofit to remove that / character check (this is a last resort)
b) Truncate the href before using the method from the interface (which I would like to avoid at all cost as well as would add unnecessary transformation all over the place.
c) Intercept the request and correctly form the URL in case this scenario happens (really ugly solution as well).
Any idea, suggestions?
Thanks!

I think this link will help you Path Replacement
Your new implementation will look like this.
#GET("/")
void login(Callback<User> callback);
You can supply a custom Endpoint implementation on which you can change the relative path.
public final class CustomEndpoint implements Endpoint {
private static final String BASE = "http://192.168.1.64:5050/api/";
private String url;
private String href;
public CustomEndpoint(String href){
this.href = href;
url = BASE + this.href;
}
#Override public String getName() {
return "default";
}
#Override public String getUrl() {
if (url == null) throw new IllegalStateException("relative path not set.");
return url;
}
}
Usage is as follows
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(new CustomEndPoint(object.href));
then restadapter.create........
Hope this will help you.

Related

Using retrofit to get url from different relative paths

I am trying to get the CompanyEndpoint for each client's site but I am confused with the use of retrofit on the interface.
Here's what I have so far:
CompanyName : "company1"
CompanyEndpoint : "https://example.com"
IdentityEndpoint : "https://example.com/identity"
AppLoginMode : "Anonymous"
AppRouterApi.java
public interface AppRouterApi {
#GET("api/sites/{CompanyName}")
Call<Company> getCompanyName (#Url String companyName);
}
Company.java
public class Company {
String Endpoint;
public String getEndpoint() {
return endpoint;
}
}
MainActivity.java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
appRouterApi = retrofit.create(AppRouterApi.class);
getCompany();
}
private void getCompany(){
retrofit2.Call<Company> companyRequest = appRouterApi.getCompanyName(); //Error here saying a string cant be applied to ()
companyRequest.enqueue(new retrofit2.Callback<Company>() {
#Override
public void onResponse(retrofit2.Call<Company> call, retrofit2.Response<Company> response) {
if(!response.isSuccessful()){
textViewResult.setText("Code:" + response.code());
return;
}
Company company = response.body();
String content = "";
content += "Url" + company.getEndpoint();
textViewResult.setText(content);
}
#Override
public void onFailure(retrofit2.Call<Company> call, Throwable t) {
}
});
}
https://example/sites/{companyName}
So if I search for:
https://example/sites/company1
The JSON will have one object and I need to get the endpoint URL value which would be: https://company1.com
Edit: My textViewReslt is returning 403
There are several things going on as far as I can tell. Let me break it into chunks.
First thing is you're confusing the annotation #Path with the annotation #Url. They serve different purposes.
You use #Path when you want to format a bit of the path into the url inside the annotations like #GET.
public interface AppRouterApi {
#GET("api/sites/{CompanyName}")
Call<Company> getCompanyName (#Path("CompanyName") String companyName);
}
This interface will format the argument passed to getCompanyName as part of the path. Calling getCompanyName("foo") will call the endpoint "https://example.com/api/sites/foo".
You use #Url when you want to simply call that url. In this case, you only annotate the interface method with the http method. For example,
public interface AppRouterApi {
#GET
Call<Company> getCompanyName (#Url String url);
}
You then would have to call the method with the entire url. To call the same url as before you'd have to call getCompanyName("https://example.com/api/sites/foo").
This is the main difference of usage between these 2 annotations. The reason why you're seeing null in your text view is because you're model's attribute name doesn't match the json. You have 2 options.
First, you can change the model to:
public class Company {
String CompanyEndpoint;
public String getEndpoint() {
return endpoint;
}
}
CompanyEndpoint is the exact same name as you have in the json. Another approach, is to tell your json serializer what name you want to use. Since you're using gson, you can use #SerializedName like so:
public class Company {
#SerializedName("CompanyEndpoint")
String Endpoint;
public String getEndpoint() {
return endpoint;
}
}
#SerializedName("CompanyEndpoint") tells gson which name to use while serializing and deserializing.
In essence, you have 2 options. You either use the endpoint, or the company's name. If you don't expect the domain to change, I'd suggest using the first approach with the #Path annotation. This is what it's usually done with Retrofit and personally, I think it's easier to handle than passing urls around. My suggestion is, use a model like:
public class Company {
#SerializedName("CompanyName")
String name;
public String getName() {
return name;
}
}
This would let you access the company's name property and call getCompanyName(company.getName()). Retrofit would format the company's name into the path and you'd call the right url.

Send a name/value pair as object in Multipart using Retrofit

In my code I have to make a MultipartForm-Data PUT request to update an object in the server, it ought to be Multipart as it is possible the user will send an image together with the data.
In order to do that, I am currently using Retrofit since it's a library I'm decently used to and it's working to send images to the server.
However, things have changed server-side and now one of the parameters that must be sent is:
{"step":
{"type":"begin"}
}
However that's been proving to be surprisingly hard to do.
Things I have tried include passing it as a MultipartTypedOutput, a hand-typed String and a JSONObject converted to String, all of which gave me:
retrofit.RetrofitError: 400 Bad Request
The URL being used is correct, I've double checked with the person who maintains the server and it is reaching the server, but with an incorrect "step" object.
I've also tried passing it as NameValuePair, Map and HashMap, all of which gave me:
retrofit.RetrofitError: Part body must not be null.
#FieldPart which looks to be perfect for this isn't compatible with Multipart, so is there a way to do this with Retrofit at all?
My current PUT method is as such:
#Headers({
"Connection: Keep-Alive",
"Accept-Language: en-US"
})
#Multipart
#PUT("/0.1/user/{id}")
String updateUser(#Path("id") String userId, #Part("step") Map<String,String> type);
Where the Map type has been changed to all the types I mentioned before.
You are actually doing it the correct way,just need some quick fix. I have two suggestions for you,
1. You can create a innerclass like this
public class Example {
#SerializedName("type")
#Expose
private String type;
/**
*
* #return
* The type
*/
public String getType() {
return type;
}
/**
*
* #param type
* The type
*/
public void setType(String type) {
this.type = type;
}
}
In this case, your api will look like
#Multipart
#PUT("/0.1/user/{id}")
String updateUser(#Path("id") String userId, #Part("step") Example exp);
or, you can use the JsonObject, this is not the Apache JSONObject that you have already used. Its the google gson object which comes under package of com.google.gson. Here you need to do the following,
JsonObject settingObject = new JsonObject();
settingObject.addProperty("type", "begin");
In this case, it will be,
String updateUser(#Path("id") String userId, #Part("step") JsonObject obj);
This is all you need, btw you can also set the header for once and all, why bother defining it over an api ?
I still don't understand what the issue is, as the "Part body must not be null" occurred even when trying out what Ankush mentioned.
Either way, I spoke to a few friends and a few contact expansions later, I got the following solution:
#Headers({
"Connection: Keep-Alive",
"Accept-Language: en-US"
})
#Multipart
#PUT("/0.1/user/{id}")
String updateUser(#Path("id") String userId, #Part("step[type]") String type);
As far as I could find, this isn't mentioned anywhere in retrofit's documentation, but it does work.

Retrofit 2 removes characters after hostname from base url

I'm using Retrofit to access a RESTful api. The base url is:
http://api.example.com/service
This is the code for the interface:
public interface ExampleService {
#Headers("Accept: Application/JSON")
#POST("/album/featured-albums")
Call<List<Album>> listFeaturedAlbums();
}
and this is how I send request and receive the responce:
new AsyncTask<Void, Void, Response<List<Album>>>() {
#Override
protected Response<List<Album>> doInBackground(Void... params) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://api.example.com/service")
.addConverterFactory(GsonConverterFactory.create())
.build();
ExampleService service = retrofit.create(ExampleService.class);
try {
return service.listFeaturedAlbums().execute();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
#Override
protected void onPostExecute(Response<List<Album>> listCall) {
Log.v("Example", listCall.raw().toString());
}
}.execute();
the log that I get is the weird thing:
V/Example﹕ Response{protocol=http/1.1, code=404, message=Not Found, url=http://api.example.com/album/featured-albums}
What's going on here?
Retrofit 2 uses the same rules that an <a href=""> would.
The leading / on your relative URL tells Retrofit that it is an absolute path on the host. Here's an example from a presentation I gave showing this:
Note the incorrect URL which was resolved at the bottom.
By removing the leading /, the URL then becomes relative and will combine with the path segments which are part of the base URL. Corrected in the presentation the final URL is now correct:
In your example you do not have a trailing / on the base URL. You probably want to add one so that relative paths are resolved on top of it rather than as a sibling of it.

Using Square's Tape library how do i handle calling abstract classes

I'm using Square's Tape library and i've run in to a requirement where i basically need to have an abstract TapeTask class. The problem though is that the deserialization process for the GsonConverter (which implements the library's FileObjectQueue.Converter - as demonstrated in the sample project) doesn't play well with interfaces/abstract classes.
I thought it was a Gson deserialization problem so i registered my Gson instance with a custom TypeAdapter, but that still doesn't do the trick. I figure it has something to do with the FileObjectQueue.Converter.
I'm currently trying to work around this problem, with a nasty wrapper callback interface from my sub-tasks.
My requirement is to have a single TapeQueue and be able to send in multiple types of tasks. So I have a TapeTask abstract class and have concrete implementations like ImageDownloadTask, ImageUploadTask, UrlPostWithRetrofitTask, GoogleAnalyticsTrackerTask ... etc. all going in to a single queue.
Is there a way to achieve this. I guess my question boils down to:
What do i need to do to make the FileObjectQueue.Converter play well with abstract classes?
hint :P : The javadoc for that class says "..you need to also include the concrete class name in the serialized byte array" but i'm not sure what that means. If anyone could post an explanation of how the name can be included in the serialized byte array, in a way that achieves my purpose, i'd be grateful!
I went ahead and wrote an Abstract Gson Convertor. I don't think it's super-efficient but gets the job done:
/**
* Use GSON to serialize classes to a bytes.
*
* This variant of {#link GsonConverter} works with anything you throw at it.
* It is however important for Gson to be able to understand your inner complex objects/entities
* Use an {#link InterfaceAdapter} for these purposes.
*
*/
public class GsonAbstractClassConverter<T>
implements FileObjectQueue.Converter<T> {
public static final String CONCRETE_CLASS_NAME = "concrete_class_name";
public static final String CONCRETE_CLASS_OBJECT = "concrete_class_object";
private final Gson _gson;
public GsonAbstractClassConverter(Gson gson) {
_gson = gson;
}
#Override
public T from(byte[] bytes) {
Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes));
JsonObject completeAbstractClassInfoAsJson = _gson.fromJson(reader, JsonObject.class);
Class<T> clazz;
try {
String className = completeAbstractClassInfoAsJson.get(CONCRETE_CLASS_NAME).getAsString();
clazz = (Class<T>) Class.forName(className);
} catch (ClassNotFoundException e) {
Timber.e(e, "Error while deserializing TapeTask to a concrete class");
return null;
}
String objectDataAsString = completeAbstractClassInfoAsJson.get(CONCRETE_CLASS_OBJECT)
.getAsString();
return _gson.fromJson(objectDataAsString, clazz);
}
#Override
public void toStream(T object, OutputStream bytes) throws IOException {
Writer writer = new OutputStreamWriter(bytes);
JsonObject completeAbstractClassInfoAsJson = new JsonObject();
completeAbstractClassInfoAsJson.addProperty(CONCRETE_CLASS_NAME, object.getClass().getName());
completeAbstractClassInfoAsJson.addProperty(CONCRETE_CLASS_OBJECT, _gson.toJson(object));
_gson.toJson(completeAbstractClassInfoAsJson, writer);
writer.close();
}
}

Retrofit 1.6: Call RestService with different (TCP)-Ports

I have to communicate with the following four RESTServices.
Germany (Default): http://url.com/suggest?query=
Austria http://url.com:82/suggest?query=
Swiss: http://url.com:83/suggest?query=
Spain: http://url.com:84/suggest?query=
Basically I have to call the same RESTService on different TCP-Ports for each Country. When I create a Retrofit-RestAdapter, I have to provide a Endpoint (base-url):
RestAdapter.Builder builder = new RestAdapter.Builder();
ObjectMapper mapper = new ObjectMapper();
builder.setEndpoint("http://url.com");
If I want to access those four RESTServices mentioned above, do I have to create a RestAdapter for each of them? Or is it possible to use only one RestAdapter-instance?
I tried to solve the problem by adding the TCP-Port as part of the RestInterface-annotation, but this does not work:
public interface AutoSuggestRemote {
#GET (":{port}/suggest")
public Response getSuggestions(#Path ("port") Integer httpPort, #Query ("query") String query);
}
I get the following exception in Logcat:
java.lang.IllegalArgumentException: AutoSuggestRemote.getSuggestions: URL path ":{port}/suggest" must start with '/'.
at retrofit.RestMethodInfo.methodError(RestMethodInfo.java:123)
at retrofit.RestMethodInfo.parsePath(RestMethodInfo.java:212)
at retrofit.RestMethodInfo.parseMethodAnnotations(RestMethodInfo.java:165)
at retrofit.RestMethodInfo.init(RestMethodInfo.java:133)
at retrofit.RestAdapter$RestHandler.invokeRequest(RestAdapter.java:294)
at retrofit.RestAdapter$RestHandler.invoke(RestAdapter.java:240)
at $Proxy3.getSuggestions(Native Method)
Therefore my question, if I have to create a RestAdapter-instance for each RESTService, or is there a way to communicat with all four services by using the same RestAdapter-instance.
Retrofit consults the EndPoint class each times it does a request. As previously answered by #JakeWharton in the question Dynamic Paths in Retrofit you could extend the EndPoint class with your own implementation and dynamically set the appropriate port as desired.
Here's the code provided by #JakeWharton modified for your specific purpose.
public final class FooEndpoint implements Endpoint {
private static final String BASE = "http://192.168.1.64:";
private String url;
public void setPort(String port) {
url = BASE + port;
}
#Override public String getName() {
return "default";
}
#Override public String getUrl() {
if (url == null) throw new IllegalStateException("port not set.");
return url;
}
}
You can then use the reference to this FooEndPoint instance to change the port dynamically or once when you initialise.
If you choose to set the port once when initialized then you would simply do this.
FooEndPoint endPoint = new FooEndPoint();
endPoint.setPort(loadPortFromSomeWhere());
RestAdapter.Builder builder = new RestAdapter.Builder();
builder.setEndpoint(endPoint);
This will allow you to use a single RestAdapter with multiple ports.

Categories

Resources