I am building a client/server application, and am encapsulating the request and response messages in generic Java classes that looks like this:
@SuperBuilder
@Jacksonized
@Getter
@Setter
public class AppResponse<T extends AppResponse.Results> {
public static final String SUCCESS_ATTRIBUTE = "success";
public static final String MESSAGE_ATTRIBUTE = "message";
public static final String REQUEST_ID_ATTRIBUTE = AppRequest.REQUEST_ID_ATTRIBUTE;
public static final String RESULTS_ATTRIBUTE = "results";
@JsonProperty(SUCCESS_ATTRIBUTE)
boolean success;
@JsonProperty(MESSAGE_ATTRIBUTE)
String message;
@JsonProperty(REQUEST_ID_ATTRIBUTE)
String requestID;
@JsonProperty(RESULTS_ATTRIBUTE)
T results;
@SuperBuilder
@Jacksonized
@Getter
@Setter
public static class Results {
}
}
Then I have one class that extends AppResponse.Results for each command response packet that can be sent over the wire. For example:
@SuperBuilder
@Getter
@Setter
@Jacksonized
public class AppCreateForumPostResults extends AppResponse.Results {
@JsonProperty("id")
UUID id;
}
So when the server encodes this to JSON using Jackson, I get a JSON document that looks like this:
{
"success" : true,
"requestID" : "eL55P4Sz",
"results" : {
"id" : "0b48d389-2407-43c4-ad1b-ab52599c2d7f"
}
}
When the client application receives this message, it first decodes it to an ObjectNode:
objectNode = (ObjectNode) AppObjectMapper.OBJECT_MAPPER.readTree(message);
It then decodes that into a generic object, because it does not yet know what the concrete type for the generic is yet:
JavaType genericType = AppObjectMapper.OBJECT_MAPPER.getTypeFactory().constructParametricType(
AppResponse.class,
AppResponse.Results.class
);
AppResponse<AppResponse.Results> genericResponse = AppObjectMapper.OBJECT_MAPPER.treeToValue(
objectNode,
genericType
);
Then it consults a HashMap of requestIDs that have been sent to find out what kind of results class it should expect. The HashMap is defined as:
private final Map<String, CallbackEntry<? extends AppResponse.Results>> callbacks = new HashMap<>();
And CallbackEntry is defined as:
@Builder
@Value
public static class CallbackEntry<T extends AppResponse.Results> {
Callback<T> method;
Class<T> resultsClass;
}
And finally Callback is defined as:
public interface Callback<T extends AppResponse.Results> {
void handleResponse(AppResponse<T> results);
}
So once we have found the requestID in the HashMap, we know what concrete class the results contain, and we can decode it:
CallbackEntry<? extends AppResponse.Results> callbackEntry = callbacks.get(genericResponse.getRequestID());
callbacks.remove(genericResponse.getRequestID());
JavaType concreteType = AppObjectMapper.OBJECT_MAPPER.getTypeFactory().constructParametricType(
AppResponse.class,
callbackEntry.resultsClass
);
AppResponse<? extends AppResponse.Results> concreteResponse = AppObjectMapper.OBJECT_MAPPER.treeToValue(
objectNode,
concreteType
);
So far, so good. This all works exactly as expected. If I print out the concreteResponse object, it has exactly the correct data in it. However, when I go to actually call the callback method, I'm getting an error. The call looks like:
callbackEntry.method.handleResponse(concreteResponse);
And the error I get on that line of code is:
[127,49] incompatible types: com.whatever.app.common.AppResponse<capture#1 of ? extends com.whatever.app.common.AppResponse.Results> cannot be converted to com.whatever.app.common.AppResponse<capture#2 of ? extends com.whatever.app.common.AppResponse.Results>
I'm a bit confused by the error message. I'm guessing it has to do somehow with the way in which the generics are being handled, but I just can't figure out what the correct incantation is to get this to go. I've done a bunch of Googling, and find similar errors here and there, but they generally are due to someone using ? instead of T, which (I think) is not the case here.
So, what am I missing?
Any help is greatly appreciated!
CodePudding user response:
I was able to get this to work by casting the function parameter like this:
callbackEntry.method.handleResponse(
(concreteResponse.getClass()).cast(concreteResponse)
);
That shows a warning for an unchecked assignment, but it works.