springfox(swagger2) does not work with GsonHttpMessageConverterConfig - spring-boot

What I am trying to build is a spring-boot (v1.2.3) application and expose my Rest API with SpringFox(swagger2) v2.0.0
my Swagger Spring config
#EnableSwagger2
#Configuration
public class SwaggerConfig {
#Bean
public Docket myApi() {
return new Docket(DocumentationType.SWAGGER_2)
.genericModelSubstitutes(DeferredResult.class)
.useDefaultResponseMessages(false)
.forCodeGeneration(false)
.pathMapping("/my-prj");
}
}
I need to use gson to convert my pojo's to json, and I do it this way:
#Configuration
public class GsonHttpMessageConverterConfig {
#Bean
public GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson) {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
converter.setGson(gson);
return converter;
}
}
The trouble is that if using GsonHttpMessageConverter, swagger v2 generates a wrong json:
{
"value": "{\"swagger\":\"2.0\",\"info\":{\"description\":\"Api Documentation\",\"version\":\"1.0\",\"title\":\"Api Documentation\",\"termsOfService\":\"urn:tos\",\"contact\":{\"name\":\"Contact Email\"},\"license\":{\"name\":\"Apache 2.0\",\"url\":\"http:
...
the JSON is prefixed with value and the real JSON becomes an escaped string.
here is how it should be if not using GsonHttpMessageConverter:
{
"swagger": "2.0",
"info": {
"description": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a
...
Is there a solution to create a correct swagger JSON without value and escaping?

solved the issue by myself:
the issue was with serializing this class:
package springfox.documentation.spring.web.json;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.annotation.JsonValue;
public class Json {
private final String value;
public Json(String value) {
this.value = value;
}
#JsonValue
#JsonRawValue
public String value() {
return value;
}
}
to serialize it correct I implemented a SpringfoxJsonToGsonAdapter and added it to my gson config:
adapter:
public class SpringfoxJsonToGsonAdapter implements JsonSerializer<Json> {
#Override
public JsonElement serialize(Json json, Type type, JsonSerializationContext context) {
final JsonParser parser = new JsonParser();
return parser.parse(json.value());
}
}
gson config:
#Configuration
public class GsonHttpMessageConverterConfig {
#Bean
public GsonHttpMessageConverter gsonHttpMessageConverter() {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
converter.setGson(gson());
return converter;
}
private Gson gson() {
final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter());
return builder.create();
}
}

This is Oleg Majewski's solution for SpringFox + Gson problem translated to Kotlin:
internal class SpringfoxJsonToGsonAdapter : JsonSerializer<Json> {
override fun serialize(json: Json, type: Type, context: JsonSerializationContext): JsonElement
= JsonParser().parse(json.value())
}
#Configuration
open class GsonHttpMessageConverterConfig {
#Bean
open fun gsonHttpMessageConverter(): GsonHttpMessageConverter {
val converter = GsonHttpMessageConverter()
converter.gson = gson()
return converter
}
private fun gson(): Gson = GsonBuilder()
.registerTypeAdapter(Json::class.java, SpringfoxJsonToGsonAdapter())
.create()
}

Ran into a similar problem but found a little different solution which is also using the above mentioned serializer.
We define a Bean to be able to autowire Gson objects. For fixing the issue with Swagger the important part there is to also add "registerTypeAdapter" for the Json class.
#Configuration
public class GsonConfiguration {
#Bean
public Gson gson() {
return new GsonBuilder().registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter()).create();
}
}
The content of SpringfoxJsonToGsonAdapter is the same as above and only listed here for completeness.
public class SpringfoxJsonToGsonAdapter implements JsonSerializer<Json> {
#Override
public JsonElement serialize(Json json, Type type, JsonSerializationContext context) {
final JsonParser parser = new JsonParser();
return parser.parse(json.value());
}
}
For using the Gson object just do something like this:
#Component
public class Foobar {
#Autowired
Gson gson;
#Autowired
public Foobar() {
// ... some constructor work ...
}
public void someMethod() {
System.out.println(gson.toJson(...)); // Fill in some object ;-)
}
}

This is the same solution as Oleg Majowski's. I am just getting rid of the SpringfoxJsonToGsonAdapter class using a lambda function instead:
#Bean
public GsonHttpMessageConverter gsonHttpMessageConverter() {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
converter.setGson(gson());
return converter;
}
private Gson gson() {
final GsonBuilder builder = new GsonBuilder();
JsonSerializer<Json> jsonSerializer =
(Json json, Type type, JsonSerializationContext context) -> new JsonParser().parse(json.value());
builder.registerTypeAdapter(Json.class, jsonSerializer);
return builder.create();
}

A couple of things I found missing with the above instructions is the package and imports. When I first tried this, I used my own packages but swagger-ui.html still said there were no packages found. It appears the package is specific.
The classes below are exactly the same as above, but I included the entire class with package names and imports. Registering the adapter is the same as documented above.
First the JSON class
package springfox.documentation.spring.web.json;
import com.fasterxml.jackson.annotation.*;
public class Json {
private final String value;
public Json(String value) {
this.value = value;
}
#JsonValue
#JsonRawValue
public String value() {
return value;
}
}
and the adapter class:
package springfox.documentation.spring.web.json;
import java.lang.reflect.Type;
import com.google.gson.*;
public class SpringfoxJsonToGsonAdapter implements com.google.gson.JsonSerializer<Json> {
#Override
public JsonElement serialize(Json json, Type type, JsonSerializationContext context) {
final JsonParser parser = new JsonParser();
return parser.parse(json.value());
}
}

Related

Spring Mongo Populator one by one

I'm using MongoDB and Spring over Kotlin and i want my application to populate a MongoDB collection upon startup. (and clean it every time it starts)
My question is, how can i populate the data one by one in order to be fault tolerant in case some of the data I'm populating with is problematic?
my code:
#Configuration
class IndicatorPopulator {
#Value("classpath:indicatorData.json")
private lateinit var data: Resource
#Autowired
private lateinit var indicatorRepository: IndicatorRepository
#Bean
#Autowired
fun repositoryPopulator(objectMapper: ObjectMapper): Jackson2RepositoryPopulatorFactoryBean {
val factory = Jackson2RepositoryPopulatorFactoryBean()
indicatorRepository.deleteAll()
factory.setMapper(objectMapper)
factory.setResources(arrayOf(data))
return factory
}
What I am looking for is something like:
#Bean
#Autowired
fun repositoryPopulator(objectMapper: ObjectMapper): Jackson2RepositoryPopulatorFactoryBean {
val factory = Jackson2RepositoryPopulatorFactoryBean()
indicatorRepository.deleteAll()
factory.setMapper(objectMapper)
val arrayOfResources: Array<Resource> = arrayOf(data)
for (resource in arrayOfResources){
try{
factory.setResources(resource)
} catch(e: Exception){
logger.log(e.message)
}
}
return factory
}
Any idea on how to do something like that would be helpful...
Thanks in advance.
There is no built in support for your ask but you can easily provide by tweaking few classes.
Add Custom Jackson 2 Reader
public class CustomJackson2ResourceReader implements ResourceReader {
private static final Logger logger = LoggerFactory.getLogger(CustomJackson2ResourceReader.class);
private final Jackson2ResourceReader resourceReader = new Jackson2ResourceReader();
#Override
public Object readFrom(Resource resource, ClassLoader classLoader) throws Exception {
Object result;
try {
result = resourceReader.readFrom(resource, classLoader);
} catch(Exception e) {
logger.warn("Can't read from resource", e);
return Collections.EMPTY_LIST;
}
return result;
}
}
Add Custom Jackson 2 Populator
public class CustomJackson2RepositoryPopulatorFactoryBean extends Jackson2RepositoryPopulatorFactoryBean {
#Override
protected ResourceReader getResourceReader() {
return new CustomJackson2ResourceReader();
}
}
Configuration
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
public AbstractRepositoryPopulatorFactoryBean repositoryPopulator(ObjectMapper objectMapper, KeyValueRepository keyValueRepository) {
Jackson2RepositoryPopulatorFactoryBean factory = new CustomJackson2RepositoryPopulatorFactoryBean();
keyValueRepository.deleteAll();
factory.setMapper(objectMapper);
factory.setResources(new Resource[]{new ClassPathResource("badclassname.json"), new ClassPathResource("good.json"), new ClassPathResource("malformatted.json")});
return factory;
}
}
I've uploading a working example here
Using Sagar's Reader & Factory I just adjusted it to fit my needs (Kotlin, and reading resources all from the same JSON file) got me this answer:
#Configuration
class IndicatorPopulator {
#Value("classpath:indicatorData.json")
private lateinit var data: Resource
#Autowired
private lateinit var indicatorRepository: IndicatorRepository
#Autowired
#Bean
fun repositoryPopulator(objectMapper: ObjectMapper): Jackson2RepositoryPopulatorFactoryBean {
val factory: Jackson2RepositoryPopulatorFactoryBean = CustomJackson2RepositoryPopulatorFactoryBean()
factory.setMapper(objectMapper)
// inject your Jackson Object Mapper if you need to customize it:
indicatorRepository.deleteAll()
val resources = mutableListOf<Resource>()
val readTree: ArrayNode = objectMapper.readTree(data.inputStream) as ArrayNode
for (node in readTree){
resources.add( InputStreamResource(node.toString().byteInputStream()))
}
factory.setResources(resources.toTypedArray())
return factory
}
}

Set Spring SolrDocument Collection name based on PropertyValue

I want to set values Spring SolrDocument Collection based on application.yml value.
#Data
#SolrDocument(collection = #Value("${solr.core}"))
public class SearchableProduct {
}
Hoi Michela,
Ok, I had the same Problem and I found a solution: SpEL
it is described in details here:Spring Data for Apache Solr
you have to add the EL-expression to the Annotation
#SolrDocument(collection = "#{#serverSolrContext.getCollectionName()}")
public class SOLREntity implements Serializable {
.....
}
you have to provide a the serverSolrContext Bean with the method getCollectionName().
#Value("${solr.core}")
private String core;
public String getCollectionName() {
return core;
}
you have to write in our application.properties the following core entry.
solr.core=myOwnCoreName
That's it actually, BUT
if you get the following Exception, so as I did:
org.springframework.expression.spel.SpelEvaluationException: EL1057E: No bean resolver registered in the context to resolve access to bean
You have to have the following in your Configuration Bean
#Configuration
#EnableSolrRepositories(basePackages = { "de.solr.db" })
#Profile("default")
#PropertySource("classpath:application.properties")
public class ServerSolrContext extends AbstractSolrConfiguration {
private static final Logger logger = LoggerFactory.getLogger(ServerSolrContext.class);
#Resource
private Environment environment;
#Value("${solr.core}")
private String core;
public String getCollectionName() {
return core;
}
#PostConstruct
public void init() {
System.out.println(core);
}
#Bean
public SolrClient solrClient() {
String url = environment.getProperty("solr.server.url");
String user = environment.getProperty("solr.server.user");
String password = environment.getProperty("solr.server.password");
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
new UsernamePasswordCredentials(user, password));
SSLContext sslContext = null;
try {
sslContext = ReportConfiguration.getTrustAllContext();
}
catch (KeyManagementException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
HttpClient httpClient = HttpClientBuilder.create().setSSLSocketFactory(sslSocketFactory)
.addInterceptorFirst(new PreemptiveAuthInterceptor()).setDefaultCredentialsProvider(credentialsProvider)
.build();
SolrClient client = new HttpSolrClient.Builder().withHttpClient(httpClient).withBaseSolrUrl(url).build();
return client;
}
#Bean
#ConditionalOnMissingBean(name = "solrTemplate")
public SolrTemplate solrTemplate(#Qualifier("mySolrTemplate") SolrTemplate solrTemplate) {
return solrTemplate;
}
#Bean("mySolrTemplate")
public SolrTemplate mySolrTemplate(SolrClient solrClient, SolrConverter solrConverter) {
return new SolrTemplate(new HttpSolrClientFactory(solrClient), solrConverter);
}
#Override
public SolrClientFactory solrClientFactory() {
return new HttpSolrClientFactory(solrClient());
}
}
The last 3 Methods are doing the Trick, that cost me a while to find the right solution:
it is here, so actually I was lucky to find this:
Allow PropertyPlaceholders in #SolrDocument solrCoreName

Spring Data Redis with JSON converters gives "Path to property must not be null or empty."

I am trying to use a CrudRepository in association with spring-data-redis and lettuce. Following all the advice I can find I have configured my spring-boot 2.1.8 application with #ReadingConverters and #WritingConverters but when I try to use the repository I am getting "Path to property must not be null or empty."
Doing some debugging, this seems to be caused by org.springframework.data.redis.core.convert.MappingRedisConverter:393
writeInternal(entity.getKeySpace(), "", source, entity.getTypeInformation(), sink);
The second parameter being the path. This ends up at line 747 of MappingRedisConverter running this code:
} else if (targetType.filter(it -> ClassUtils.isAssignable(byte[].class, it)).isPresent()) {
sink.getBucket().put(path, toBytes(value));
}
Ultimately, the put with an empty path ends up in org.springframework.data.redis.core.convert.Bucket:77 and fails the Assert.hasText(path, "Path to property must not be null or empty."); even though the data has been serialized.
Is this a bug with spring-data-redis or have I got to configure something else?
RedicsConfig.java
#Configuration
#EnableConfigurationProperties({RedisProperties.class})
#RequiredArgsConstructor
#EnableRedisRepositories
public class RedisConfiguration {
private final RedisConnectionFactory redisConnectionFactory;
#Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(redisConnectionFactory);
template.afterPropertiesSet();
return template;
}
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.findAndRegisterModules();
return objectMapper;
}
#Bean
public RedisCustomConversions redisCustomConversions(List<Converter<?,?>> converters) {
return new RedisCustomConversions(converters);
}
}
I've just included one writing converter here but have several reading and writing ones...
#Component
#WritingConverter
#RequiredArgsConstructor
#Slf4j
public class CategoryWritingConverter implements Converter<Category, byte[]> {
private final ObjectMapper objectMapper;
#Setter
private Jackson2JsonRedisSerializer<Category> serializer;
#Override
public byte[] convert(Category category) {
return getSerializer().serialize(category);
}
private Jackson2JsonRedisSerializer<Category> getSerializer() {
if (serializer == null) {
serializer = new Jackson2JsonRedisSerializer<>(Category.class);
serializer.setObjectMapper(objectMapper);
}
return serializer;
}
}
The object to write:
#Data
#JsonInclude(JsonInclude.Include.NON_NULL)
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#AllArgsConstructor
#NoArgsConstructor
#RedisHash("category")
#TypeAlias("category")
public class Category {
#Id
#EqualsAndHashCode.Include
private String categoryCode;
private String categoryText;
}
And the repo:
public interface CategoryRepository extends CrudRepository<Category, String> {
Page<Category> findAll(Pageable pageable);
}
Can anybody advise what I have missed or if this is a bug I should raise on spring-data-redis?

How to configure i18n in Spring boot 2 + Webflux + Thymeleaf?

I just start a new project based on Spring boot 2 + Webflux. On upgrading version of spring boot and replace spring-boot-starter-web with spring-boot-starter-webflux classes like
WebMvcConfigurerAdapter
LocaleResolver
LocaleChangeInterceptor
are missing. How now can I configure defaultLocale, and interceptor to change the language?
Just add a WebFilter that sets the Accept-Language header from the value of a query parameter. The following example gets the language from the language query parameter on URIs like http://localhost:8080/examples?language=es:
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import reactor.core.publisher.Mono;
import static org.springframework.util.StringUtils.isEmpty;
#Component
public class LanguageQueryParameterWebFilter implements WebFilter {
private final ApplicationContext applicationContext;
private HttpWebHandlerAdapter httpWebHandlerAdapter;
public LanguageQueryParameterWebFilter(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
#EventListener(ApplicationReadyEvent.class)
public void loadHttpHandler() {
this.httpWebHandlerAdapter = applicationContext.getBean(HttpWebHandlerAdapter.class);
}
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
final ServerHttpRequest request = exchange.getRequest();
final MultiValueMap<String, String> queryParams = request.getQueryParams();
final String languageValue = queryParams.getFirst("language");
final ServerWebExchange localizedExchange = getServerWebExchange(languageValue, exchange);
return chain.filter(localizedExchange);
}
private ServerWebExchange getServerWebExchange(final String languageValue, final ServerWebExchange exchange) {
return isEmpty(languageValue)
? exchange
: getLocalizedServerWebExchange(languageValue, exchange);
}
private ServerWebExchange getLocalizedServerWebExchange(final String languageValue, final ServerWebExchange exchange) {
final ServerHttpRequest httpRequest = exchange.getRequest()
.mutate()
.headers(httpHeaders -> httpHeaders.set("Accept-Language", languageValue))
.build();
return new DefaultServerWebExchange(httpRequest, exchange.getResponse(),
httpWebHandlerAdapter.getSessionManager(), httpWebHandlerAdapter.getCodecConfigurer(),
httpWebHandlerAdapter.getLocaleContextResolver());
}
}
It uses #EventListener(ApplicationReadyEvent.class) in order to avoid cyclic dependencies.
Feel free to test it and provide feedback on this POC.
With spring-boot-starter-webflux, there are
DelegatingWebFluxConfiguration
LocaleContextResolver
For example, to use a query parameter "lang" to explicitly control the locale:
Implement LocaleContextResolver, so that
resolveLocaleContext() returns a SimpleLocaleContext determined by a GET parameter of "lang". I name this implementation QueryParamLocaleContextResolver. Note that the default LocaleContextResolver is an org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver.
Create a #Configuration class that extends DelegatingWebFluxConfiguration. Override DelegatingWebFluxConfiguration.localeContextResolver() to return QueryParamLocaleContextResolver that we just created in step 1. Name this configuration class WebConfig.
In WebConfig, override DelegatingWebFluxConfiguration.configureViewResolvers() and add the ThymeleafReactiveViewResolver bean as a view resolver. We do this because, for some reason, DelegatingWebFluxConfiguration will miss ThymeleafReactiveViewResolver after step 2.
Also, I have to mention that, to use i18n with the reactive stack, this bean is necessary:
#Bean
public MessageSource messageSource() {
final ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:/messages");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(5);
return messageSource;
}
After creating a natural template, some properties files, and a controller, you will see that:
localhost:8080/test?lang=zh gives you the Chinese version
localhost:8080/test?lang=en gives you the English version
Just don't forget <meta charset="UTF-8"> in <head>, otherwise you may see some nasty display of Chinese characters.
Starting with Spring Boot 2.4.0, the WebFluxAutoConfiguration contains a bean definition for the LocaleContextResolver, which allows us to inject custom LocaleContextResolver. For reference, the following is the default bean definition in Spring Boot 2.5.4 (the implementation may be different in earlier versions):
#Bean
#Override
#ConditionalOnMissingBean(name = WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public LocaleContextResolver localeContextResolver() {
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
return new FixedLocaleContextResolver(this.webProperties.getLocale());
}
AcceptHeaderLocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
localeContextResolver.setDefaultLocale(this.webProperties.getLocale());
return localeContextResolver;
}
You can provide your own LocaleContextResolver implementation to get the locale from the query parameter by providing a custom bean definition:
//#Component("localeContextResolver")
#Component(WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public class RequestParamLocaleContextResolver implements LocaleContextResolver {
#Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
List<String> lang = exchange.getRequest().getQueryParams().get("lang");
Locale targetLocale = null;
if (lang != null && !lang.isEmpty()) {
targetLocale = Locale.forLanguageTag(lang.get(0));
}
if (targetLocale == null) {
targetLocale = Locale.getDefault();
}
return new SimpleLocaleContext(targetLocale);
}
#Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
throw new UnsupportedOperationException(
"Cannot change lang query parameter - use a different locale context resolution strategy");
}
}
Note that the framework consumes the LocaleContextResolver with a specific name localeContextResolver (WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME). You need to provide the bean with the given name. See #24209.
Another solution with spring boot starter web flux, which is much more cleaner, is to define your own HttpHandler using WebHttpHandlerBuilder in which you can set your LocaleContextResolver.
Documentation (see 1.2.2. WebHandler API) : https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-config-customize
MyLocaleContextResolver.java
public class MyLocaleContextResolver implements LocaleContextResolver {
#Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
return new SimpleLocaleContext(Locale.FRENCH);
}
#Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
throw new UnsupportedOperationException();
}
}
Then in a config file (annotated with #Configuration) or in your spring boot application file, defined your own HttpHandler bean.
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public HttpHandler httpHandler(ApplicationContext context) {
MyLocaleContextResolver localeContextResolver = new MyLocaleContextResolver();
return WebHttpHandlerBuilder.applicationContext(context)
.localeContextResolver(localeContextResolver) // set your own locale resolver
.build();
}
}
That's it!
Based on Jonatan Mendoza's answer, but simpliefied and in kotlin:
/**
* Override Accept-Language header by "lang" query parameter.
*/
#Component
class LanguageQueryParameterWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val languageValue = exchange.request.queryParams.getFirst("lang") ?: ""
if (languageValue.isEmpty()) {
return chain.filter(exchange)
}
return chain.filter(
exchange.mutate().request(
exchange.request
.mutate()
.headers {
it[HttpHeaders.ACCEPT_LANGUAGE] = languageValue
}
.build(),
).build(),
)
}
}

Implement AttributeConverter for Generic Class with #Converter

How do I implement AttributeConverter for Generics?
Something Like
class JSONConverter<T> implements AtttributeConverter<T,String>{
//Here How do I get the generic class type with which I can convert a serialized object
}
call the converter in an entity class as
#Column
#Convert( converter = JSONConverter.class) //How do I pass the Generic here
private SomeClass sm;
Can be done even a bit simpler, because you don't necessarily need to pass typeReference via constructor:
public abstract class JsonConverter<T> implements AttributeConverter<T, String> {
private final TypeReference<T> typeReference = new TypeReference<T>() {};
#Resource
private ObjectMapper objectMapper;
#Override
public String convertToDatabaseColumn(T object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
#Override
public T convertToEntityAttribute(String json) {
try {
return objectMapper.readValue(json, typeReference);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
and then class, that extends your JsonConventer can look like:
public class SomeClassConverter extends JsonConverter<SomeClass> { }
I used the following solution with Eclipselink and Java 8. One issue that wasn't immediately apparent is that the converter class must implement AttributeConverter directly (at least for it to work with Eclipselink)
Step 1. Define a generic interface to implement Object <-> String Json conversion. Because interfaces cannot contain properties I defined two methods getInstance() and getObjectMapper() to provide the conversion logic access to object instances that it requires at run time. Converter classes will need to provide implementations for these methods.
package au.com.sfamc.fusion.commons.jpa;
import java.io.IOException;
import javax.persistence.AttributeConverter;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public interface GenericJsonAttributeConverter<X> extends AttributeConverter<X, String> {
X getInstance();
ObjectMapper getObjectMapper();
#Override
default String convertToDatabaseColumn(X attribute) {
String jsonString = "";
try {
// conversion of POJO to json
if(attribute != null) {
jsonString = getObjectMapper().writeValueAsString(attribute);
} else {
jsonString = "{}"; // empty object to protect against NullPointerExceptions
}
} catch (JsonProcessingException ex) {
}
return jsonString;
}
#Override
default X convertToEntityAttribute(String dbData) {
X attribute = null;
try {
if(StringUtils.isNoneBlank(dbData)) {
attribute = getObjectMapper().readValue(dbData, (Class<X>)getInstance().getClass());
} else {
attribute = getObjectMapper().readValue("{}", (Class<X>)getInstance().getClass());
}
} catch (IOException ex) {
}
return attribute;
}
}
Step 2. The getObjectMapper() method implementation would be repeated every converter class so I introduced an abstract class that extends GenericJsonAttributeConverter to save having to implement this method in every converter class.
package au.com.sfamc.fusion.commons.jpa;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractGenericJsonAttributeConverter<X> implements GenericJsonAttributeConverter<X> {
private static final ObjectMapper objectmapper = new ObjectMapper();
#Override
public ObjectMapper getObjectMapper() {
return AbstractGenericJsonAttributeConverter.objectmapper;
}
}
Step 3. Create a concrete implementation of AbstractGenericJsonAttributeConverter for each class you want to to convert to and from Json, even though each class you want to convert will need its own concrete converter class at least you aren't duplicating the conversion code...
package au.com.sfamc.fusion.main.client;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import au.com.sfamc.fusion.commons.jpa.AbstractGenericJsonAttributeConverter;
#Converter
public class ProjectMetricsReportJsonConverter extends AbstractGenericJsonAttributeConverter<ProjectMetricsReport> implements AttributeConverter<ProjectMetricsReport, String> {
private static final ProjectMetricsReport projectMetricsReport = new ProjectMetricsReport();
#Override
public ProjectMetricsReport getInstance() {
return ProjectMetricsReportJsonConverter.projectMetricsReport;
}
}
Note: Now the trick to get this to work with Eclipselink is subtle but required. Along with extending AbstractGenericJsonAttributeConverter the concrete implementation must also make a direct implements reference to the `AttributeConverter' interface (a small price to pay to get generic conversion working)
This can be done a lot simpler.
This example is using Hibernate 5.6.4, Jackson 2.13.1, and Spring DI.
public abstract class JsonConverter<T> implements AttributeConverter<T, String> {
private TypeReference<T> typeReference;
#Resource
private ObjectMapper objectMapper;
public JsonConverter(TypeReference<T> typeReference) {
this.typeReference = typeReference;
}
#Override
public String convertToDatabaseColumn(T object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
#Override
public T convertToEntityAttribute(String json) {
try {
return objectMapper.readValue(json, typeReference);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
and then to use it just extend JsonConverter and provide a TypeReference e.g.
public class SomeClassConverter extends JsonConverter<SomeClass> {
public SomeClassConverter() {
super(new TypeReference<SomeClass>() {
});
}
}

Resources