0

I am currently trying to develop a solution within Quarkus that can serialize JSON responses dynamically based on the properties (i.e. query params, headers, etc.) of the associated request. Here's an example of what I am trying to achieve.

  1. The user sends a GET request containing a query param lang which specifies the language.
@GET()
@Produces(MediaType.APPLICATION_JSON)
public Content getContent(
    @QueryParam("lang")
    @DefaultValue("en")
    Language language
) {
    // Fetch and return content from service
}
  1. The Content class contains fields of type TranslatableString, which store keys used for lookups.
public @Value class TranslatableString {
    String key;
}
  1. The associated JsonSerializer should now take this custom type and use its key to provide the translation for the requested language.
@ApplicationScoped
public class Serializer extends JsonSerializer<TranslatableString> {

    @Inject
    TranslationStore translations;

    @Override
    public void serialize(TranslatableString value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(
            translations.get(value.getKey(), /** ??-Language-?? **/ )
        );
    }
}

Is there some (easy) way to access the user-provided query parameters inside the Jackson JsonSerializer?

EDIT

This is how I register my custom serializer.

@Singleton
public class ObjectMapperConfig implements ObjectMapperCustomizer {

    @Inject
    Serializer serializer;

    @Override
    public void customize(ObjectMapper objectMapper) {
        objectMapper.registerModule(
            new SimpleModule()
                    .addSerializer(
                            TranslatableString.class,
                            serializer
                    )
        );
    }
}
krakowski
  • 85
  • 1
  • 7
  • 1
    I'd convert the `Content` object to a DTO with all translations filled in, and return that DTO. – Rob Spoor Dec 30 '22 at 14:49
  • I might try this solution. This means you would let the mapper take care of translating each field? – krakowski Dec 30 '22 at 15:57
  • 1
    Not the `ObjectMapper`, but before that. Either manually or using some mapping code generation like MapStruct. – Rob Spoor Dec 30 '22 at 16:11
  • Thanks, by mapper I meant the `@Mapper` annotated interface using MapStruct. On second thought, this might be more complicated than I thought, since the `Content` class also contains nested fields which in turn have `TranslatableString` instances. This would only work with Reflection/`instanceof`-like code, in my opinion. Haven't found a MapStruct feature which can "serialize" between two types (without specifying field names) using custom logic, yet. – krakowski Dec 30 '22 at 16:26
  • You can try to use [ThreadLocal](https://www.baeldung.com/java-threadlocal). Try to create a bean which uses `ThreadLocal` to set and get `Language`. Set the language in controller method and load it in serialiser. – Michał Ziober Dec 30 '22 at 16:41
  • 1
    @RobSpoor, I figured out how to create a simple implementation using MapStruct's Context feature. I will post this solution underneath my question, since it works pretty well and requires minimal extra code. – krakowski Dec 30 '22 at 16:44
  • There are framework specific ways to get what you are asking. Are you using RESTEasy Reactive? – geoand Jan 03 '23 at 05:42

1 Answers1

1

Based on @RobSpoor's comment, I came up with the following solution using MapStruct and a DTO instead of relying on Jackson to do the translation.

  1. Create domain specific and DTO class.
public @Value class Content {
    TranslatableString body;
}

// Instances of Content will be mapped to ContentDto by using MapStruct

public @Value class ContentDto {
    String body;
}
  1. Create MapStruct mapper for mapping from domain object to DTO. Use MapStruct's Context feature to pass in language during mapping.
@Mapper(componentModel = "cdi")
public abstract class ContentMapper {

    // Dummy translations
    private final Map<Language, Map<String, String>> translations = Map.of(
            Language.ENGLISH, Map.of(
                    "greeting", "Hello World"
            ),

            Language.GERMAN, Map.of(
                    "greeting", "Hallo Welt"
            )
    );

    // The language parameter will be passed to the translate method
    abstract ContentDto toDto(Content content, @Context Language language);

    // Use the language parameter to retrieve the right translation
    protected String translate(TranslatableString translatableString, @Context Language language) {
        return translations.get(language).get(translatableString.getKey());
    }
}
  1. Pass language into mapper during service call.
@Path("/greeting")
public class GreetingResource {

    @Inject
    ContentMapper mapper;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public ContentDto evaluate(
            @QueryParam("lang")
            @DefaultValue("en")
            Language language
    ) {
        return mapper.toDto(
            new Content(new TranslatableString("greeting")),
            language
        );
    }
}

Now /greeting can be called with an extra query parameter of lang specifying the desired language.

krakowski
  • 85
  • 1
  • 7
  • Nice, I was wondering how you were going to do this. I haven't worked with `@Context` yet, but its intent is pretty clear from your example. – Rob Spoor Dec 30 '22 at 17:04