I am working on a Spring Boot application and there I use JWE - tokens. When generating these tokens I serialize a given DTO. As an example, the generation of an AccessToken looks like this:
public Jwe generateAccessToken(AccessTokenDto dto) throws Exception {
return generateToken(
dto.getId().toString(),
Map.of(
"dto", dto,
"dtoClassName", dto.getClass().getName()
),
config.getAccessTokenExpiration()
);
}
private Jwe generateToken(String subject, Map<String, Object> claims, long expiration) throws Exception {
var claimSetBuilder = new JWTClaimsSet.Builder()
.subject(subject)
.issuer(config.getIssuer())
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + 1000 * expiration));
for (Map.Entry<String, Object> claimEntry : claims.entrySet()) {
if (claimEntry.getValue() instanceof String value) {
claimSetBuilder.claim(claimEntry.getKey(), value);
} else {
claimSetBuilder.claim(claimEntry.getKey(), serialise(claimEntry.getValue()));
}
}
return new Jwe(
claimSetBuilder.build(),
config.signingKey(),
config.encryptionKey()
);
}
private String serialise(Object dto) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(dto);
oos.close();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
while the AccessTokenDto itself looks like this:
@Data
public class AccessTokenDto implements Serializable {
@ValidUserId
private UUID id;
@ValidUserRole
private Role role;
}
When I later want to get from a already, successfully parsed Jwe, I use these three methods:
public <T> T toDto() throws IOException {
Class<T> type = (Class<T>) getDtoClass();
Object deserializedDto = deserialize();
if (!type.isInstance(deserializedDto)) {
return null;
}
return type.cast(deserializedDto);
}
private Class<?> getDtoClass() throws IOException {
String className = (String) claims.getClaim("dtoClassName");
Class<?> type = null;
try {
type = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new IOException("When deserialzing Token - No Class with Name: " + className);
}
return type;
}
private Object deserialize() {
Object serializedDto = claims.getClaim("dto");
if (!(serializedDto instanceof String)) {
return null;
}
byte[] data = Base64.getDecoder().decode(serializedDto.toString());
try {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
Object obj = ois.readObject();
ois.close();
return obj;
} catch (Exception e) {
System.out.println("Error while deserializing token");
}
return null;
}
I know this code works and I also know that there shouldn't be a problem since the "dtoClassName" claim is allays set correctly when generating a token. (As long as I code the method to generate the tokens right.) But IntelliJ gives a warning about an unchecked cast, which I would like to get rid of.
How do I safely cast to the generic in the first line of this method?
public <T> T toDto() throws IOException {
Class<T> type = (Class<T>) getDtoClass();
Object deserializedDto = deserialize();
if (!type.isInstance(deserializedDto)) {
return null;
}
return type.cast(deserializedDto);
}
My attempts for a solution/workaround:
I experimented a bit with Optionals
public Optional<?> toDto() {
Class<?> targetType = getDtoClass();
Object deserializedDto = deserialize();
if (!targetType.isInstance(deserializedDto)) {
return Optional.empty();
}
return Optional.of(targetType.cast(deserializedDto));
}
to work around the problem, but that did not go well since I wanted to be able to access the methods of the DTO, and that was not possible.
Then I searched for ways to check the cast and found the isAssignableFrom(Class<T> class) method, but I could not get it to work. I also tried ways of using isInstance, but that led to nothing.
I read somewhere that such a scenario can not be checked because it is a compile time issue, not a runtime issue, but I am not sure.
So what this question really is about is how to safely cast
In the context of type safety and generics, "safely cast" is an oxymoron. Type safety is about the compiler being able to prove the safety of type conversions at build time. A cast is what you fall back on when you want to perform a conversion whose safety is contingent on information that is not available to or not recognized by the compiler. Such conversions are unsafe by definition.
And please do not think that I am nitpicking your terminology. Considering your
Class<T> type = (Class<T>) getDtoClass();
, the getDtoClass() method returns a Class<?>. In that case, if the conversion is in fact safe in a broad sense then imputing class parameter T to the result is a perfect example of performing a type conversion whose safety is contingent on information that is not available to the compiler. If the compiler doesn't have the needed information then it doesn't have it. Other than doing without, which doesn't seem viable for you, there can be no typesafe workaround for that.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With