POST x-www-form-urlencoded?

I have a few service calls that have to receive POST bodies in x-www-form-urlencoded format. Is there a recommended approach for creating a MessageSerializer for those, ideally in a somewhat generic manner (suitable for a MessageSerializer factory)?

Thanks!

Hi Stephen, is there something you’ve tried that worked? I’d like to see how you went about it.

Try to use FormUrlEncodedParser#parseAsJavaArrayValues for parsing body.

Hi Anyaebosi,

I haven’t executed an implementation yet, I’ve been in a preliminary research mode as it’s not something we’ve absolutely needed yet, but it’s coming up soon.

So far I’ve not had any concrete ideas. My instincts make me feel like maybe there’s some way of making Jackson JSON help even though the data isn’t JSON, but I’ve not really made a lot of use of that library lately so while I suspect there’s something it can help with in terms of object serialization/deserialization I’m not sure where/how.

Here’s a SerializerFactory that can handle x-www-form-urlencoded content-type. Maybe not optimal but it gives an idea of one way to go about it.

public class FormSerializerFactory implements SerializerFactory {

    @Override
    public <MessageEntity> StrictMessageSerializer<MessageEntity> messageSerializerFor(Type type) {
        return new GenericMessageSerializer<>(type);
    }
}


public class GenericMessageSerializer<MessageEntity>
        implements StrictMessageSerializer<MessageEntity> {

    private final Type type;
    private final ObjectMapper objectMapper;

    public GenericMessageSerializer(Type type) {
        this.type = type;
        this.objectMapper = new ObjectMapper();
    }

    @Override
    public NegotiatedSerializer<MessageEntity, ByteString> serializerForRequest() {
       return new PlainTextSerializer("utf-8");
    }

    @Override
    public NegotiatedDeserializer<MessageEntity, ByteString> deserializer(
            MessageProtocol protocol) throws UnsupportedMediaType {

        if (protocol.contentType().isPresent()) {
            if (protocol.contentType().get().equals("application/x-www-form-urlencoded")) {
                return new FormEncodedDeserializer(type, objectMapper);
            }  else if (protocol.contentType().get().equals("application/json")) {
                return new JsonDeserializer(type, objectMapper);
            }  else if (protocol.contentType().get().equals("text/plain")) {
                return new PlainTextDeserializer("utf-8");
            }  else {
                throw new UnsupportedMediaType(protocol, new MessageProtocol().withContentType("text/plain"));
            }
        } else {
            return new PlainTextDeserializer("utf-8");
        }
    }

    @Override
    public NegotiatedSerializer<MessageEntity, ByteString> serializerForResponse(
            List<MessageProtocol> acceptedMessageProtocols) throws NotAcceptable {
        if (acceptedMessageProtocols.isEmpty()) {
            return new PlainTextSerializer("utf-8");
        } else {
            for (MessageProtocol protocol: acceptedMessageProtocols) {
                if (protocol.contentType().isPresent()) {
                    String contentType = protocol.contentType().get();
                    if (contentType.equals("text/plain") || contentType.equals("text/*") || contentType.equals("*/*")) {
                        return new PlainTextSerializer(protocol.charset().orElse("utf-8"));
                    } else if (protocol.contentType().get().equals("application/json")) {
                        return new JsonSerializer();
                    }
                } else {
                    return new PlainTextSerializer(protocol.charset().orElse("utf-8"));
                }
            }
            throw new NotAcceptable(acceptedMessageProtocols, new MessageProtocol().withContentType("text/plain"));
        }
    }

    public class PlainTextSerializer implements NegotiatedSerializer<MessageEntity, ByteString> {
        private final String charset;

        public PlainTextSerializer(String charset) {
            this.charset = charset;
        }

        @Override
        public MessageProtocol protocol() {
            return new MessageProtocol(
                    Optional.of("text/plain"),
                    Optional.of(charset),
                    Optional.empty()
            );
        }

        @Override
        public ByteString serialize(MessageEntity s) throws SerializationException {
            if (s instanceof String) {
                return ByteString.fromString((String) s, charset);
            }
            try {
                return ByteString.fromString(objectMapper
                        .writerFor(objectMapper.constructType(type)).writeValueAsString(s));
            } catch (JsonProcessingException e) {
                throw new SerializationException(e);
            }
        }
    }

    private class JsonSerializer implements NegotiatedSerializer<MessageEntity, ByteString> {
        private final ObjectMapper mapper = new ObjectMapper();

        @Override
        public MessageProtocol protocol() {
            return new MessageProtocol(
                    Optional.of("application/json"),
                    Optional.empty(), Optional.empty());
        }

        @Override
        public ByteString serialize(MessageEntity s) throws SerializationException {
            try {
                return ByteString.fromArray(mapper.writeValueAsBytes(s));
            } catch (JsonProcessingException e) {
                throw new SerializationException(e);
            }
        }
    }

    private  class FormEncodedSerializer implements
            NegotiatedSerializer<String, ByteString> {

        @Override
        public MessageProtocol protocol() {
            return new MessageProtocol(
                    Optional.of("application/x-www-form-urlencoded"),
                    Optional.empty(), Optional.empty());
        }

        @Override
        public ByteString serialize(String entity) throws SerializationException {
            return ByteString.fromString(entity);
        }
    }

    private class FormEncodedDeserializer
            implements NegotiatedDeserializer<MessageEntity, ByteString> {

        private final ObjectMapper objectMapper;
        private final JavaType javaType;

        public FormEncodedDeserializer(Type type, ObjectMapper objectMapper) {
            this.objectMapper = objectMapper;
            this.javaType = objectMapper.constructType(type);
        }

        @Override
        public MessageEntity deserialize(ByteString byteString) throws DeserializationException {
            final String string
                    = byteString.decodeString("utf-8");
            final String decodedString;
            try {
                decodedString = URLDecoder.decode(string, "utf-8");
            } catch (UnsupportedEncodingException e) {
                throw new DeserializationException(e);
            }
            final Map<String, String> nameValuePair = Arrays
                    .stream(decodedString.split("&"))
                    .collect(Collectors.toMap(
                            token -> token.split("=")[0],
                            token -> token.split("=")[1]
                    ));
            return objectMapper.convertValue(nameValuePair, javaType);
        }
    }


    private class JsonDeserializer
            implements NegotiatedDeserializer<MessageEntity, ByteString> {

        private final ObjectMapper mapper;
        private final JavaType javaType;

        public JsonDeserializer(Type type, ObjectMapper mapper) {
            this.mapper = mapper;
            this.javaType = mapper.constructType(type);
        }

        @Override
        public MessageEntity deserialize(ByteString byteString)
                throws DeserializationException {
            try {
                return mapper.readValue(
                        byteString.iterator().asInputStream(), javaType);
            } catch (IOException e) {
                throw new DeserializationException(e);
            }
        }
    }

    private class PlainTextDeserializer implements NegotiatedDeserializer<MessageEntity, ByteString> {
        private final String charset;

        public PlainTextDeserializer(String charset) {
            this.charset = charset;
        }

        @Override
        public MessageEntity deserialize(ByteString byteString) throws DeserializationException {
            return (MessageEntity) byteString.decodeString(charset);
        }
    }
}