2

I'm porting a legacy API to Java from a Ruby app that takes full advantage of not being statically typed.

In one such situation, the API accepts from the JSON body a user_id that is either a numeric user ID or the string "me", (which should be converted to the ID of the user making the request before being saved to the database). The method looks like this:

@PUT
@Path("{key}")
@UnitOfWork
public Response getMyObjByKey(@PathParam("key") String key, MyObj myObj) {
    myObjDAO.save(myObj);
}

I have a converter that I want to look something like this:

public class UserIdConverter extends StdConverter<String, Integer> {

    @Inject
    @AuthUser
    protected AuthenticatedUser user;

    public Integer convert(String strUserId) {
        // if strUserId is "me"
        return user.getId();
    }
}

Of course, this doesn't work because of...something to do with the lifecycle that causes user to be null.

My question is: Is there a way for me to access the user object in the converter?

Matt
  • 2,953
  • 3
  • 27
  • 46
  • Can you not access the user with `@Auth user` in the `PUT` method and send it to the converter? – TM00 Feb 27 '18 at 10:35
  • It is accessible in the resource class, yes. I'm not sure how I would send it to the converter though since I'm not explicitly calling `convert` and I believe the deserialization happens before the first statement in the PUT method – Matt Feb 27 '18 at 13:29
  • Okay it seems I am a bit confused then. When is the `UserIdConverter` present in the transaction? Is it part of `MyObj`? – TM00 Feb 27 '18 at 15:24
  • It's just an implementation of Jackson's `StdConverter`. The model is annotated with `@JacksonDeserialize(converter = UserIdConverter.class)` and Jackson does whatever it does with that. – Matt Feb 27 '18 at 15:30
  • I see. I will suggest moving the conversion itself to the resource method as I don't see a way to get the authenticated user during the deserialization process. i.e. convert the number if necessary, then save to the DB. You can the get the user with `@Auth`. – TM00 Feb 27 '18 at 15:40
  • Right, my problem is that deserialization happens before the method is even entered. That is, since the POJO stores `user_id` as an int, an exception is thrown since it can't save the string `"me"` to `MyObj`. Therefore I cannot convert it within the resource method. I suppose I can just change the model to accept a string and then convert the string to an int before saving to the DB but that adds a bunch of conversions around the app that I'd prefer not to do. – Matt Feb 27 '18 at 15:44

1 Answers1

0

To my knowledge its not possible to access the User during the deserialization. Try a custom deserializer and place an indicating int as the id if the String "me" is received. Something like:

public class MyObjDeserializer extends JsonDeserializer<MyObj>{

    @Override
    public MyObj deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {

        ObjectCodec oc = jp.getCodec();
        JsonNode node = oc.readTree(jp);

        MyObj myobj = new MyObj();
        myobj.setName(node.get("name").textValue());
        // set other attributes...

        String idString = node.get("id").textValue();
        int id = -1;
        if(!idString.equals("me")) { 
           // another option: use idString.matches("-?\\d+") to check if its a number - https://stackoverflow.com/a/15801999/7015661
            id = Integer.parseInt(idString);
        }

        myobj.setId(id);

        return myobj;
    }

}

Then in your MyObj class place @JsonDeserialize(using = MyObjDeserializer.class) to use your own deserializer.

Now if the id is "-1", you can get the user's own id in the resource method as follows:

@PUT
@Path("{key}")
@UnitOfWork
public Response getMyObjByKey(@PathParam("key") String key, MyObj myObj, @Auth Optional<User> user) {

    if(myObj.getId() == -1){
        if(!user.isPresent) // something went very wrong
             throw new RuntimeException("User not authenticated");

       myObj.setId(user.get().getId()); // set the id to its correct value
    }

    myObjDAO.save(myObj);
}

Hope this works :)

TM00
  • 1,330
  • 1
  • 14
  • 26
  • Thank you for your answer. This is actually what I'm currently doing but was hoping there was a cleaner way (since many endpoints use `MyObj`, and forgetting to copy the -1 check to any one of them could result in invalid values being saved to the DB). I think I'll keep this question open for a bit to make sure nobody has a better solution. – Matt Feb 27 '18 at 18:10
  • My pleasure. Your right, copying the check is quite tedious. Hopefully someone has a more elegant solution. I'll keep an eye on the question as I'm curious to see how such a solution can be implemented. – TM00 Feb 27 '18 at 18:38
  • Looks like a `!` is missing into `if(idString.equals("me"))` – Ôrel Feb 28 '18 at 16:51
  • Well spotted! I fixed it. Thanks. – TM00 Feb 28 '18 at 16:56