Error deserializing parameter in WCF service from Android kSOAP - android

8/5/11: Update near bottom of post
I'm trying to call my WCF web service method from my Android app using kSOAP. The method takes 4 parameters. 3 of them are of type Guid (for which marshalling Java's UUID works), and the last is a custom type: LocationLibrary.Location. This type is in a separate DLL (LocationLibrary) that I load into the WCF web service project and it is basically comprised of two doubles, latitude and longitude.
[OperationContract]
byte[] GetData(Guid deviceId, Guid appId, Guid devKey, LocationLibrary.Location loc);
The Location class in the project LocationLibrary is very simple:
namespace LocationLibrary
{
public class Location
{
public double latitude { get; set; }
public double longitude { get; set; }
public new string ToString()
{
return latitude.ToString() + "," + longitude.ToString();
}
public bool Equals(Location loc)
{
return this.latitude == loc.latitude && this.longitude == loc.longitude;
}
}
}
In my Android project, I've created a class named "Location" that is similar to the .NET version:
public class Location {
public double latitude;
public double longitude;
public Location() {}
public static Location fromString(String s)
{
//Format from SOAP message is "anyType{latitude=39.6572799682617; longitude=-78.9278602600098; }"
Location result = new Location();
String[] tokens = s.split("=");
String lat = tokens[1].split(";")[0];
String lng = tokens[2].split(";")[0];
result.latitude = Double.parseDouble(lat);
result.longitude = Double.parseDouble(lng);
return result;
}
public String toString()
{
return Double.toString(latitude) + "," + Double.toString(longitude);
}
}
When using kSOAP to connect to the web service, I do the following:
private final String SOAP_ACTION = "http://tempuri.org/IMagicSauceV3/GetData";
private final String OPERATION_NAME = "GetData";
private final String WSDL_TARGET_NAMESPACE = "http://tempuri.org/";
private final String SOAP_ADDRESS = "http://mydomain.com/myservice.svc";
private final SoapSerializationEnvelope envelope;
SoapObject request = new SoapObject(WSDL_TARGET_NAMESPACE, OPERATION_NAME);
request.addProperty(Cloud8Connector.deviceIdProperty);
request.addProperty(Cloud8Connector.appIdProperty);
request.addProperty(Cloud8Connector.devKeyProperty);
PropertyInfo locationProperty = new PropertyInfo();
locationProperty.setName("loc");
locationProperty.setValue(Cloud8Connector.myLoc);
locationProperty.setType(Location.class);
request.addProperty(locationProperty);
envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
envelope.dotNet = true;
envelope.setOutputSoapObject(request);
MarshalUUID mu = new MarshalUUID();
mu.register(envelope);
MarshalLocation ml = new MarshalLocation();
ml.register(envelope);
byte[] result = null;
HttpTransportSE httpRequest = new HttpTransportSE(SOAP_ADDRESS);
try
{
httpRequest.call(SOAP_ACTION, envelope);
String payloadString = ((SoapPrimitive)(envelope.getResponse())).toString();
result = Base64.decode(payloadString, Base64.DEFAULT);
}
catch(Exception e)
{
e.printStackTrace();
}
As you can see, I've created a simple Marshal class for Location, which basically just uses the fromString and toString methods. I register the envelope with the marshal instance for Location:
MarshalLocation ml = new MarshalLocation();
ml.register(envelope);
Here's the marshal class for Location:
public class MarshalLocation implements Marshal
{
public Object readInstance(XmlPullParser parser, String namespace, String name,
PropertyInfo expected) throws IOException, XmlPullParserException {
return Location.fromString(parser.nextText());
}
public void register(SoapSerializationEnvelope cm) {
cm.addMapping(cm.xsd, "Location", Location.class, this);
}
public void writeInstance(XmlSerializer writer, Object obj) throws IOException {
writer.text(((Location)obj).toString());
}
}
However, I get this error returned from WCF and cannot seem to get it to work. From the error and from searching, I think I need to tweak something with my web service, but I'm not sure what exactly is the best way to get around this.
I've tried adding this to the web.config file of the web service:
<system.runtime.serialization>
<dataContractSerializer>
<declaredTypes>
<add type="Location, LocationLibrary, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null">
<knownType type="Location, LocationLibrary, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null"></knownType>
</add>
</declaredTypes>
</dataContractSerializer>
</system.runtime.serialization>
And I've tried adding the ServiceKnownType attribute to the interface method signature:
[OperationContract]
[ServiceKnownType(typeof(LocationLibrary.Location))]
byte[] GetData(Guid deviceId, Guid appId, Guid devKey, LocationLibrary.Location loc);
But I still get this error :(
Can you point me in the right direction? Thanks!
Error:
07-30 00:42:08.186: WARN/System.err(8723): SoapFault - faultcode:
'a:DeserializationFailed' faultstring: 'The formatter threw an
exception while trying to deserialize the message: There was an error
while trying to deserialize parameter http://tempuri.org/:loc. The
InnerException message was 'Error in line 1 position 522. Element
'http://tempuri.org/:loc' contains data from a type that maps to the
name 'http://www.w3.org/2001/XMLSchema:Location'. The deserializer has
no knowledge of any type that maps to this name. Consider using a
DataContractResolver or add the type corresponding to 'Location' to
the list of known types - for example, by using the KnownTypeAttribute
attribute or by adding it to the list of known types passed to
DataContractSerializer.'. Please see InnerException for more details.'
faultactor: 'null' detail: org.kxml2.kdom.Node#4053aa28
Update
Ideally, I wouldn't have to move the LocationLibrary classes inside of the main WCF project since that will complicate handling of legacy clients that expect it in a different namespace. However, I did get things to work by moving those classes inside of the main WCF project AND modifying the marshal class in java to this:
public void register(SoapSerializationEnvelope cm) {
cm.addMapping("http://schemas.datacontract.org/2004/07/MagicSauceV3", "Location", Location.class, this);
}
public void writeInstance(XmlSerializer writer, Object obj) throws IOException {
Location loc = (Location)obj;
writer.startTag("http://schemas.datacontract.org/2004/07/MagicSauceV3", "latitude");
writer.text(Double.toString(loc.latitude));
writer.endTag("http://schemas.datacontract.org/2004/07/MagicSauceV3", "latitude");
writer.startTag("http://schemas.datacontract.org/2004/07/MagicSauceV3", "longitude");
writer.text(Double.toString(loc.longitude));
writer.endTag("http://schemas.datacontract.org/2004/07/MagicSauceV3", "longitude");
}
Now that I got that to work, I just get it working with the LocationLibrary WCF classes how they were (in a separate DLL). So I've modified the marshal class as such:
public void register(SoapSerializationEnvelope cm) {
cm.addMapping("http://schemas.datacontract.org/2004/07/LocationLibrary", "Location", Location.class, this);
}
public void writeInstance(XmlSerializer writer, Object obj) throws IOException {
Location loc = (Location)obj;
writer.startTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "latitude");
writer.text(Double.toString(loc.latitude));
writer.endTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "latitude");
writer.startTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "longitude");
writer.text(Double.toString(loc.longitude));
writer.endTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "longitude");
}
This seems like it should succeed since it produces similar XML to the WP7 version that does work.
WP7 working XML request:
<GetData xmlns="http://tempuri.org/">
<deviceId>{valid GUID}</deviceId>
<appId>{valid GUID}</appId>
<devKey>{valid GUID}</devKey>
<loc xmlns:d4p1="http://schemas.datacontract.org/2004/07/LocationLibrary" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<d4p1:latitude>47.65</d4p1:latitude>
<d4p1:longitude>-122.34</d4p1:longitude>
</loc>
</GetData>
Android non-working XML request:
<GetData xmlns="http://tempuri.org/" id="o0" c:root="1">
<deviceId i:type="d:UUID">{valid GUID}</deviceId>
<appId i:type="d:UUID">{valid GUID}</appId>
<devKey i:type="d:UUID">{valid GUID}</devKey>
<loc i:type="n0:Location" xmlns:n0="http://schemas.datacontract.org/2004/07/LocationLibrary">
<n0:latitude>47.65</n0:latitude>
<n0:longitude>-122.34</n0:longitude>
</loc>
</GetData>
The above Android XML request produces this response error:
08-05 22:51:23.703: WARN/System.err(1382): SoapFault - faultcode: 'a:DeserializationFailed' faultstring: 'The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:loc. The InnerException message was 'Error in line 1 position 590. Element 'http://tempuri.org/:loc' contains data from a type that maps to the name 'http://schemas.datacontract.org/2004/07/LocationLibrary:Location'. The deserializer has no knowledge of any type that maps to this name. Consider using a DataContractResolver or add the type corresponding to 'Location' to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding it to the list of known types passed to DataContractSerializer.'. Please see InnerException for more details.' faultactor: 'null' detail: org.kxml2.kdom.Node#4054bcb8
The only notable difference I see is with the property of "loc" in the working XML request:
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
I don't know if that would make a difference (and it doesn't seem to be referenced with the nested tags). I also don't know how to add that extra namespace without messing up the other one.
Sorry for the long post but I wanted to make sure you have all of the info you need to help :)
Thanks!

Well as I expected you are doing just ToString but it is wrong. Your location is not transported like string of two concatenated values. It is transported as object serialized to XML. Something like:
<Location> <!-- or loc -->
<latitude>10.0</latitude>
<longitude>-20.0</longitude>
</Location>

Thanks to Ladislav for the point in the right direction! I'm not sure what the etiquette is here to reward that, but I'd be glad to follow if someone would advise.
The answer was two-fold, and I can confirm that it now works with the LocationLibrary's Location class in a separate DLL:
Change how I was forming the XML request in Android using my marshaling class. The writeInstance method was only using the .textMethod of the XmlSerializer class, calling .toString() on the Location class. I changed it to utilize the proper way of forming XML tags (.startTag and .endTag methods):
public void writeInstance(XmlSerializer writer, Object obj) throws IOException {
Location loc = (Location)obj;
writer.startTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "latitude");
writer.text(Double.toString(loc.latitude));
writer.endTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "latitude");
writer.startTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "longitude");
writer.text(Double.toString(loc.longitude));
writer.endTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "longitude");
}
Add some simple tags to the WCF service. I added [DataContract] to the Location class in the LocationLibrary assembly utilized by the WCF service, and I added [DataMember] to each of its members:
[DataContract]
public class Location
{
[DataMember]
public double latitude { get; set; }
[DataMember]
public double longitude { get; set; }
public new string ToString()
{
return latitude.ToString() + "," + longitude.ToString();
}
public bool Equals(Location loc)
{
return this.latitude == loc.latitude && this.longitude == loc.longitude;
}

Related

Lambda expression returning null android

I'm writing a lambda expression to convert the given latitude and longitude into an address. The expression is supposed to take co-ordinates as arguments and returns their corresponding address. However, the value returned is null. Following is my class:
public class LambdaDeclarations {
String loc;
private static final String TAG = "LambdaDeclarations";
public CoordinatesToAddressInterface convert = (latitude, longitude, context) -> {
RequestQueue queue = Volley.newRequestQueue(context);
Log.d(TAG, "onCreate: Requesting: Lat: "+latitude+" Lon: "+longitude);
String url ="https://maps.googleapis.com/maps/api/distancematrix/json?units=metric&origins="+latitude+","+longitude+"&destinations="+latitude+","+longitude+"&key=AIzaSyCdKSW0glin4h9sGYa_3hj0L83zI0NsNRo";
// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
(String response) -> {
try {
JSONObject jsonObject = new JSONObject(response);
JSONArray destinations = jsonObject.getJSONArray("destination_addresses");
Log.d(TAG, "GETRequest: JSON Object: "+destinations.toString());
String location = destinations.toString();
Log.d(TAG, "Location: "+location);
setLocation(location);
} catch (JSONException e) {
e.printStackTrace();
}
}, error -> Log.d(TAG, "onErrorResponse: That didn't work!"));
queue.add(stringRequest);
return getLocation();
};
public String getLocation() {
return loc;
}
public void setLocation(String location) {
this.loc = location;
}
}
Following is the output from logcat:
09-16 10:31:09.160 26525-26525/com.rmit.tejas.mad_foodtruck_2 D/LambdaDeclarations: GETRequest: JSON Object: ["77 State Route 32, West Melbourne VIC 3003, Australia"]
Location: ["77 State Route 32, West Melbourne VIC 3003, Australia"]
09-16 10:31:09.176 26525-26525/com.rmit.tejas.mad_foodtruck_2 D/LambdaDeclarations: GETRequest: JSON Object: ["111 Adderley St, West Melbourne VIC 3003, Australia"]
Location: ["111 Adderley St, West Melbourne VIC 3003, Australia"]
09-16 10:31:09.177 26525-26525/com.rmit.tejas.mad_foodtruck_2 D/LambdaDeclarations: GETRequest: JSON Object: ["4\/326 William St, Melbourne VIC 3000, Australia"]
Location: ["4\/326 William St, Melbourne VIC 3000, Australia"]
Following is my usage:
myViewHolder.textView3.setText("Location: i->"+i+" add: "+l.convert.toAddress(trackingInfos.get(i).getLatitude(),trackingInfos.get(i).getLongitude(),context));
l is an object of the class LambdaDeclarations and following is the relevant interface:
public interface CoordinatesToAddressInterface {
String toAddress(double latitude, double longitude, Context context);
}
When I try to print the coordinates from the relevant adapter they are getting printed correctly. So the location is getting set properly but when I try to access it from another class it shows me a null value for the string. Can you please advise an alternate method to extract the location from the expression?
First of all, Lambda Expression is just a anonymous class implementation, it was design to be used as a method or class argument and solve shadowing issues of anonymous class.
So in your case, you don't need it at all, just simply implement CoordinatesToAddressInterface interface as named class as usual.
Second, you used Volley wrong, the first lambda you provided to StringRequest, hereafter will be call response callback, is going to be called when HTTP request finish but the return statement
return getLocation();
will return null immediately before your setLocation(location) or even your response callback ever get executed, that why you got null every time you call convert(), though you can still see log that you print because the response callback will be executed anyway (assume that request is success).
To use response callback correctly, you have to update your UI inside callback, pretty much like this
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
public static final String TAG = "MyAdapter";
private RequestQueue mQueue;
public MyAdapter(Context context) {
this.mQueue = Volley.newRequestQueue(context);
}
public RequestQueue getMyAdapterRequestQueue() {
return this.mQueue;
}
...
#Override
public void onBindViewHolder(#NonNull final MyViewHolder holder, int position) {
String url ="some url";
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
(String response) -> {
try {
JSONObject jsonObject = new JSONObject(response);
JSONArray destinations = jsonObject.getJSONArray("destination_addresses");
Log.d(TAG, "GETRequest: JSON Object: "+destinations.toString());
String location = destinations.toString();
Log.d(TAG, "Location: "+location);
// update UI
holder.mTextView.setText(location);
} catch (JSONException e) {
e.printStackTrace();
}
}, error -> Log.d(TAG, "onErrorResponse: That didn't work!"));
stringRequest.setTag(TAG);
mQueue.add(stringRequest);
}
Of course, you can edit your interface's method signature and make your adapter implement that interface (I would rather do it this way though) but the point is that you have to process asynchronous results in callback method, never expect asynchronous operation's callback to finish before your next lines of code.
RequestQueue shouldn't be created per-request since it manages internal state that help you make request faster (caching), you can also cancel requests too in an event like phone rotation and your will get destroy, in this case, just call cancel method in Activity/Fragment's onStop()
#Override
protected void onStop () {
super.onStop();
if (myAdapter.getMyAdapterRequestQueue() != null) {
myAdapter.getMyAdapterRequestQueue().cancelAll(MyAdapter.TAG);
}
}
Response callback won't be called after you cancel request.

Creating Resource with references using spring data rest

I am using spring data rest, I have following entities exposed via spring data rest
DonationRequest
#Data
#Entity
#Table(name="donation_request",schema="public")
public class DonationRequest {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="donation_request_id")
Integer donationRequestId;
#Column(name="expiry_datetime")
Date expiryDatetime;
#Column(name="blood_group")
String bloodGroup;
#Column(name="no_of_bottles")
String noOfBottles;
#OneToOne
#JoinColumn(name="hospital_id")
Hospital hospital;
#OneToOne
#JoinColumn(name="user_data_id")
UserData requester;
#Column(name="active")
Boolean active;
}
Hospital
#Data
#Entity
#Table(name="hospital",schema="public")
public class Hospital {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="hospital_id")
Integer hospitalId;
#Column(name="name")
String name;
#Column(name="address")
String address;
#Column(name="loc",columnDefinition = "geometry")
Point loc;
}
Now I have an android client which has the same class definitions as stated above. Hospitals are cached at startup on android client. Now I want to create a donationRequest entity on server. I can do that easily by posting json of donationRequest object to /api/donationRequests. this json contains hospital object also. But the newly created donationRequest and hospital are not linked together.
Following type of json in postman does not create link:
{
"bloodGroup":"AB+",
"hospital":{
"hospitalId":1
}
}
I know that following json does create link:
{
"bloodGroup":"AB+",
"hospital":"/api/hospitals/1"
}
My question is how can I create link using first type of json as that is the natural way to serialize dontaionRequest object from android client? Also I want hospitals to be exposed via /api/hospitals, so removing that rest resource is not an option.
It can be achieved by using a custom HttpMessageConverter and defining a custom content-type which can be anything other than standard (I used application/mjson):
MHttpMessageConverter.java
public class MHttpMessageConverter implements HttpMessageConverter<Object>{
#Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
if (mediaType.getType().equalsIgnoreCase("application")
&& mediaType.getSubtype().equalsIgnoreCase("mjson"))
return true;
else
return false;
}
#Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return false;
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return new ArrayList<>(Arrays.asList(MediaType.APPLICATION_JSON));
}
#Override
public Object read(Class<?> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
ObjectMapper mapper = new ObjectMapper();
Object obj = mapper.readValue(httpInputMessage.getBody(),aClass);
return obj;
}
#Override
public void write(Object o, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
}
}
CustomRestConfiguration.java
#Configuration
public class CustomRestConfiguration extends RepositoryRestConfigurerAdapter {
#Override
public void configureHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new MHttpMessageConverter());
}
}
Spring Data REST is using HATEOAS. To refer to associated resources we have to use links to them:
Create a hospital first
POST /api/hospitals
{
//...
}
response
{
//...
"_links": [
"hostpital": "http://localhost/api/hospitals/1",
//...
]
}
Then get 'hospital' (or 'self') link and add it to the 'donationRequests' payload
POST /api/donationRequests
{
"bloodGroup":"AB+",
"hospital": "http://localhost/api/hospitals/1"
}
Another approach - create first 'donationRequests' without hospital
POST /api/donationRequests
{
//...
}
response
{
//...
"_links": [
"hostpital": "http://localhost/api/donationRequests/1/hospital"
//...
]
}
then PUT hospital to donationRequests/1/hospital using text link to hospital in your payload (pay attention to Content-Type: text/uri-list)
PUT http://localhost/api/donationRequests/1/hospital (Content-Type: text/uri-list)
http://localhost/api/hospitals/1
Info: Repository resources - The association resource
UPDATE
If it's necessary to deal without links to resources we have to make a custom rest controller.

How to pass array of custom objects in request of soap service using ksoap2 in android

I want to pass an array of objects in the request of a WCF service using ksoap2 in android.
My xml request looks like:
<tem:bookItems>
<res:bookItem>
<res:name>"abcd"</res:name>
<res:price>150</res:price>
</res:bookItem>
<res:bookItem>
<res:name>"efgh"</res:name>
<res:price>250</res:price>
</res:bookItem>
</tem:bookItems>
I was not being able to find a proper answer and I am stuck completely. Please help.
You should be interested in org.ksoap2.serialization.KvmSerializable interface in ksoap2.
For any array object you can create public abstract class LiteralArrayVector extends Vector implements KvmSerializable.
LiteralArrayVector ideally should have following:
to register your soap envelope object
public void register(SoapSerializationEnvelope envelope, String namespace, String name)
envelope.addMapping(namespace, name, this.getClass());
registerElementClass(envelope, namespace);
Define private void registerElementClass(SoapSerializationEnvelope envelope, String namespace) with envelope.addMapping(namespace, "", elementClass);
Class elementClass = getElementClass();
// In concrete class of LiteralArrayVector, say, ArrayOfBookItem, override getElementClass() of super, to return your BookItem element class
Something like:
protected Class getElementClass() {
return new BookItem().getClass();
}
and getItemDescriptor to return name of your element, "BookItem"
getPropertyInfo() of KvmSerializable should be overridden such that PropertyInfo should have name and type of your BookItem
public void getPropertyInfo(int index, Hashtable properties, PropertyInfo info) {
info.name = getItemDescriptor();
info.type = getElementClass();
}
ArrayOfBookItem should override register of LiteralArrayVector such that it calls super(...) and has new BookItem().register(envelope).
When I say register(...) I mean some way to add mapping for your elements to SoapSerializationEnvelope
public void register(SoapSerializationEnvelope envelope) {
envelope.addMapping(NAMESPACE, "BookItem", this.getClass());
}
See if it helps!!
After lot of search and investigstion, found out a very simple way to do the above. I just changed my service to accept an array of JSON object instead of list of objects. It works like charm and issue got resolved with in minutes.
here is the service request modified,
<tem:bookItems>[{"name": "abcd", "price": "150"}, { "name": "efgh", "price": "250"} ]</tem:bookItems>
And from application we pass object like,
JSONArray jsonArr = new JSONArray();
JSONObject bookObj = new JSONObject();
try {
bookObj.put("name","abcd");
bookObj.put("price","150");
} catch (JSONException e) {
e.printStackTrace();
}
jsonArr.put(bookObj);
}
//the below string will be passed to service
String bookItems = jsonArr.toString();

#Get arbitrary query parameters in Android Annotations

It seems that I am unable to set arbitrary query parameters to a #Get declaration
My endpoint looks like
http://api.lmiforall.org.uk/api/v1/ashe/estimateHours?soc=2349&coarse=true
There are a non trivial amount of parameters to this query, is there a declaration I can use to indicate this to the #Rest interface?
I tried declaring it as this, but it complains about fields being unused.
#Get("estimateHours")
ASHEFilterInfo GetEstimateHours( int soc, boolean coarse, String filters, String breakdown);
java: #org.androidannotations.annotations.rest.Get annotated method has only url variables in the method parameters
Look at AA cookbook.
Try this (not tested):
#Rest(rootUrl = "http://api.lmiforall.org.uk/api/v1/ashe")
public interface MyService {
#Get("/estimateHours?soc={soc}&coarse={coarse}&breakdown={breakdonw}&filters={filters}")
ASHEFilterInfo GetEstimateHoursFiltered( int soc, boolean coarse, String filters, String breakdown);
#Get("/estimateHours?soc={soc}&coarse={coarse}&breakdown={breakdonw}")
ASHEFilterInfo GetEstimateHours( int soc, boolean coarse, String breakdown);
}
When I needed to create #Get request with many dynamic parameteres, and some of them could be duplicated, I had resolved that problem so:
#Rest(rootUrl = "http://example.com:9080/",
converters = { GsonHttpMessageConverter.class },
interceptors = { ApiInterceptor.class })
public interface ExampleApi {
#Get("content/home/product-type/list?{filters}&domain={domain}") //filters is String like "param1=value1&param1=value2&param3=value3"
ProductTypeListResponse getProductTypeList(int domain, String filters);
}
public class ApiInterceptor implements ClientHttpRequestInterceptor {
private static final String TAG = ApiInterceptor.class.getSimpleName();
#Override
public ClientHttpResponse intercept(final HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
final QueryMultiParamsHttpRequest modifiedRequest = new QueryMultiParamsHttpRequest(request);
return execution.execute(modifiedRequest, body);
}
}
public class QueryMultiParamsHttpRequest implements HttpRequest {
private static final String TAG = QueryParametersBuilder.class.getSimpleName();
private HttpRequest httpRequest;
public QueryMultiParamsHttpRequest(final HttpRequest httpRequest) {
this.httpRequest = httpRequest;
}
#Override
public HttpMethod getMethod() {
return httpRequest.getMethod();
}
#Override
public URI getURI() {
final URI originalURI = httpRequest.getURI();
final String query = originalURI.getQuery() != null ? originalURI.getQuery().replace("%3D", "=").replace("%26", "&") : null;
URI newURI = null;
try {
newURI = new URI(originalURI.getScheme(), originalURI.getUserInfo(), originalURI.getHost(), originalURI.getPort(), originalURI.getPath(),
query, originalURI.getFragment());
} catch (URISyntaxException e) {
Log.e(TAG, "Error while creating URI of QueryMultiParamsHttpRequest", e);
}
return newURI;
}
#Override
public HttpHeaders getHeaders() {
return httpRequest.getHeaders();
}
}
So, I created a wrapper for HttpRequest, that can decode symbols "=" and "&". And this wrapper replaces original HttpRequest in ApiInterceptor. This is a little hacky solution, but it works.
I ran into this same issue and came up with a another solution that while far from ideal, works. The particular problem I was trying to solve was handling "HATEOAS" links.
What I ended up doing was creating a separate class called HATEOASClient to contain endpoint methods that would not escape the HATEOAS links passed in as params. To do that I basically just looked at an auto generated endpoint method and coped/tweaked the body in my implementation.
These methods use the same RestTemplate instance AndroidAnnotations sets up so you still get access to all the general setup you do on the RestTemplate.
For example:
public ResponseEntity<Foo> postFoo(Foo foo) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(RestHeader.AUTH_TOKEN_HEADER, getClient().getHeader(RestHeader.AUTH_TOKEN_HEADER));
httpHeaders.set(RestHeader.ACCEPT_LANGUAGE_HEADER, getClient().getHeader(RestHeader.ACCEPT_LANGUAGE_HEADER));
httpHeaders.setAuthorization(authentication);
HttpEntity<Foo> requestEntity = new HttpEntity<>(null, httpHeaders);
HashMap<String, Object> urlVariables = new HashMap<>();
urlVariables.put("link", foo.getLinks().getFooCreate().getHref());
URI expanded = new UriTemplate(getClient().getRootUrl().
concat(API_VERSION + "{link}")).expand(urlVariables);
final String url;
try {
url = URLDecoder.decode(expanded.toString(), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return getClient().getRestTemplate().
exchange(url, HttpMethod.POST, requestEntity, Foo.class, urlVariables);
}
If all parameters is required you can use #Path annotation.
#Rest(rootUrl = "http://api.lmiforall.org.uk/api/v1/ashe")
public interface MyService {
#Get("/estimateHours?soc={soc}&coarse={coarse}&breakdown={breakdown}&filters={filters}")
ASHEFilterInfo GetEstimateHours(#Path int soc, #Path boolean coarse, #Path String breakdown, #Path String filters);
}
If one of the parameters is optional, there isn't yet a solution that can you can easily pass parameters using Android Annotations. But anybody can contribute to better Android Annotations.
if you define the params for each method then you need to provide them in each request. I thought this was sort of over kill too so what I did was just make a generic get/post request in my api client then just manually enter the values, if you don't define the root url I suppose you could use the QueryStringBuilder class and build the uri that way.
#Rest(rootUrl = "https://path/to/api/", converters = { FormHttpMessageConverter.class,
GsonHttpMessageConverter.class, ByteArrayHttpMessageConverter.class })
public interface ApiClient {
#Get("{uri}")
JsonElement apiGet(String uri);
#Post("{uri}")
JsonObject apiPost(String uri,MultiValueMap data);
RestTemplate getRestTemplate();
void setRootUrl(String rootUrl);
void setRestTemplate(RestTemplate restTemplate);
}
Example usage
JsonElement resp = apiClient.apiGet("method/?random_param=1&another_param=test);
It's not as clean but can be dynamic

Conversion of primitive java Datatype double to OData Edm.Double

I am developing an Android Application which retreives the Location and sends this to a OData Server via Restlet [1]
In Android the Location sets Longitude and Latitude as double.
double lat = location.getLatitude();
My Restlet / OData Service:
public class LatLoc {
private double latitude;
public void setLatitude(double latitude) {
this.latitude = latitude;
}
}
The OData $metadata:
<EntityType Name="locations">
<Property Name="latitude" Type="Edm.Double"/>
The method responsible to add a Entity: (GeoService is the class handeling the Restlet connection and implements the method addEntity() )
public class LocTrans {
GeoService proxy = new GeoService();
void sendLoc(Location location) throws Exception, Throwable {
LatLoc locSend = new LatLoc();
locSend.setLatitude(location.getLatitude());
System.out.println("Lat")
proxy.addEntity(locSend);
}
}
The println shows a 'normal' Latitude in form XX.XXXXXXXXXXX (with at least 6 digits after the '.')
But the server receives a Latitude like XX.XXX; so only with 3 digits after the decimal point.
The documentation of Restlet staits, that Edm.Double and the primitive datatype double arer equivalent.
Where is my error, that the double values are cut off while being send to the server?
[1] http://wiki.restlet.org/developers/172-restlet/267-restlet/271-restlet.html
Sad answer: I haven't found a solution to the Restlet problem.
I am using odata4j now, and this works.
void sendLocation(Location location, UUID uuid) throws Exception {
OEntity newLocation = c.createEntity(entitySet)
.properties(OProperties.guid("trackID", uuid.toString()),
OProperties.double_("latitude", location.getLatitude()),
.execute();
}

Categories

Resources