Lazy collection in Spring JPA with Jackson and elasticsearch wihtout jsonignore - json

We have spring boot with elasticsearch and mysql. We have a feature for reindexing all data from the mysql into elasticsearch, which is simple as:
#Service
#Transactional
public class SearchIndexer {
public void reindex(){
elasticsearchRepository.save(jpaRepository.findAll());
}
}
Now we have an entity called invoice, which has a lazy loaded collection with a "derived" calculation:
#Entity
#Table(name = "invoice")
#Document(indexName = "invoice")
public class Invoice implements Serializable {
//... other props
#OneToMany(fetch = FetchType.LAZY, mappedBy = "invoice")
#JsonIgnore
private Set<InvoiceItem> invoiceItems = new LinkedHashSet<>();
// getter and setters for invoiceItems
public boolean isAllSimple() {
if(getInvoiceType()==null){
return false;
}
if(getInvoiceItems()==null){
return false;
}
for(InvoiceItem item : getInvoiceItems()){
if(!item.isSimple()){
return false;
}
}
return true;
}
}
When the rest-controller is used, the resulting json contains correctly a property "allSimple". This is, because we run that with hibernate5module in one transaction.
However, when we call elasticsearchRepository.save(jpaRepository.findAll()) (also in a transaction), the objectmapper for elasticsearch cannot serialize the "allSimple" property, beacause of a LazyInitializationException. The elasticsearch-objectmapper is configured as follows:
#Bean
public ElasticsearchTemplate elasticsearchTemplate(Client client, Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder, Hibernate5Module hibernate5Module) {
return new ElasticsearchTemplate(client, new CustomEntityMapper(jackson2ObjectMapperBuilder.createXmlMapper(false).modulesToInstall(hibernate5Module).build()));
}
public class CustomEntityMapper implements EntityMapper {
private ObjectMapper objectMapper;
public CustomEntityMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure( DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
#Override
public String mapToString(Object object) throws IOException {
return objectMapper.writeValueAsString(object);
}
#Override
public <T> T mapToObject(String source, Class<T> clazz) throws IOException {
return objectMapper.readValue(source, clazz);
}
}
The hibernate5module is loaded and registered, but did not solve the problem.
Normally we would add a "JsonIgnore" to that property, but we need that value, so this is no option.
Any ideas?!

I had a project configurated with this.
#EnableWebMvc
#Configuration
#ComponentScan(basePackages = "com.sagasoftware.tracker.*")
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
Hibernate5Module hibernate5Module = new Hibernate5Module();
objectMapper.registerModule(hibernate5Module);
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
messageConverter.setObjectMapper(objectMapper);
return messageConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(mappingJackson2HttpMessageConverter());
super.configureMessageConverters(converters);
}
}
If you are using spring boot, declaring the bean MappingJackson2HttpMessageConverter and registering the hibernate5module should fix your problem.
I could render a entity throught a rest controller.

Related

Jackson2JsonMessageConverter serializing LocalDateTime in specific pattern

I am trying to send an event using RabbitMQ and SpringBoot.
#Configuration class:
#Bean
public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
return new Jackson2JsonMessageConverter();
}
Event class:
public class TheEvent{
#JsonFormat(pattern = "dd::MM::yyyy")
private LocalDateTime date;
//setters getters
}
When I send it, it arrives as:
{"month":"JULY","year":2018,"dayOfMonth":12,"dayOfWeek":"THURSDAY","dayOfYear":193,"hour":16,"minute":29,"nano":835000000,"second":24,"monthValue":7,"chronology":{"id":"ISO","calendarType":"iso8601"}},"direction":1}"
How can I serialize this date object in predefined pattern? (Remember that I just registering bean Jackson2JsonMessageConverter)
Also tried this:
#Bean
#Primary
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
return objectMapper;
}
It works for me:
#Bean
public Jackson2JsonMessageConverter converter(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
return new Jackson2JsonMessageConverter(objectMapper);
}
In project I used RabbitListener and that Jackson2JsonMessageConverter
Listener:
#RabbitListener(queues = "${spring.rabbitmq.queue}")
#Transactional
public void receiveSocialPost(SocialPost socialPost) {
}

Spring Boot with xml and json with jackson only returns xml

Let me thank you in advance for your help!
I have a weird behaviour in an spring boot application. Let me explain it for you:
I'm wrapping some legacy web services (custom xml messages) with some nice rest-json services (via spring-mvc and spring boot and using jackson for serializing stuff)
In order to communicate with the legacy systems, I have created a custom XmlMapper, serializers and deserializers.
And finally, I have created an httpclientconfig, in order to define some http connection properties...
But after starting the app and trying to visit any endpoint (actuator ones for example), the app only returns xml. Event swagger endpoints return xml (what makes swagger-ui going nuts.
These are some of the classes:
#Configuration
public class HttpClientConfig {
private static final Logger logger = LoggerFactory.getLogger(HttpClientConfig.class);
#Value(value = "${app.http.client.max_total_connections}")
public String MAX_TOTAL_CONNECTIONS;
#Value(value = "${app.http.client.max_connections_per_route}")
public String MAX_CONNECTIONS_PER_ROUTE;
#Value(value = "${app.http.client.connection_timeout_milliseconds}")
public String CONNECTION_TIMEOUT_MILLISECONDS;
#Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}
#Autowired
private XmlMapper xmlMapper;
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter jsonConverter = (MappingJackson2HttpMessageConverter) converter;
jsonConverter.setObjectMapper(new ObjectMapper());
}
if (converter instanceof MappingJackson2XmlHttpMessageConverter) {
MappingJackson2XmlHttpMessageConverter jsonConverter = (MappingJackson2XmlHttpMessageConverter) converter;
jsonConverter.setObjectMapper(xmlMapper);
}
}
logger.debug("restTemplate object created====================================");
return restTemplate;
}
#Bean
public HttpClient httpClient() {
HttpClient httpClient = null;
try {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
// disable SSL check
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
#Override
public boolean isTrusted(java.security.cert.X509Certificate[] arg0, String arg1)
throws CertificateException {
return true;
}
}).build();
httpClientBuilder.setSSLContext(sslContext);
// don't check Hostnames
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslSocketFactory).build();
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
connMgr.setMaxTotal(Integer.parseInt(MAX_TOTAL_CONNECTIONS));
connMgr.setDefaultMaxPerRoute(Integer.parseInt(MAX_CONNECTIONS_PER_ROUTE));
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(Integer.parseInt(CONNECTION_TIMEOUT_MILLISECONDS)).build();
httpClientBuilder.setDefaultRequestConfig(config);
httpClientBuilder.setConnectionManager(connMgr);
// to avoid nohttpresponse
httpClientBuilder.setRetryHandler(new HttpRequestRetryHandler() {
#Override
public boolean retryRequest(IOException exception, int executionCount,
org.apache.http.protocol.HttpContext context) {
// TODO Auto-generated method stub
return true;
}
});
httpClient = httpClientBuilder.build();
} catch (Exception e) {
logger.error("Excption creating HttpClient: ", e);
}
return httpClient;
}
}
And the xml mapper
#Configuration
public class XmlMapperConfig{
#Bean
public XmlMapper getXmlMapper() {
XmlMapper mapper=new XmlMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(CafRequestObject.class, new CafRequestObjectSerializer());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(module);
mapper.findAndRegisterModules();
CafXmlSerializationProvider cafXmlProvider=new CafXmlSerializationProvider(new XmlRootNameLookup());
mapper.setSerializerProvider(cafXmlProvider);
return mapper;
}
}
I call to findAndregisterModules, because I am also developing some libraries which provides additional serializers for services (modularized stuff)
I'm completely lost with this. Any help would be much appreciated...
Regards!
I have solved it extending WebMvcConfigurerAdapter:
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry
.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
Thanks again!

How to make #JsonView annotated Rest apis serializes all fields

I have a rest api like "/users/{userId}"
This api returns User data but filters out password by #JsonView(ResourceView.Public.class) annotation.
But I want to get password when the unit test runs.
Is there a way to igore #JsonView annotation when test is running.
Or any other options for me?
public class ResourceView {
public interface Public {}
public interface Friends extends Public {}
public interface Family extends Friends {}
}
public class User {
#JsonView(ResourceView.Public.class)
private String name;
#JsonView(ResourceView.Family.class)
private String password;
}
#RestController
public class UserController {
#Autowired
private UserService userService;
#JsonView(ResourceView.Public.class)
#GetMapping(value = "/users/{userId}")
public User getUser(#PathVariable("userId") String userId) {
return userService.getUser(userId);
}
}
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT)
#ActiveProfiles(profiles = "test")
public class UserServiceTest {
#Autowired
protected TestRestTemplate restTemplate;
#Value("${local.server.port}")
private int port;
protected String apiEndpoint;
#Before
protected void setUp() {
initRequestContext();
apiEndpoint = "http://localhost:" + port;
}
protected ResponseEntity<User> requestGetUser(String userId) {
ResponseEntity<User> res = restTemplate.exchange(
apiEndpoint + "/users/" + userId,
HttpMethod.GET,
new HttpEntity<>("parameters", createDefaultHttpHeaders()),
new ParameterizedTypeReference<User>() {});
return res;
}
#Test
public void testGetUser() throws Exception {
ResponseEntity<User> apiRes = requestGetUsers(request);
assertThat(apiRes.getStatusCode(), is(HttpStatus.OK));
User user = apiRes.getBody();
assertThat(user.getName(), is(notNullValue()));
assertThat(user.getPassword(), is(notNullValue()));
}
}
#Configuration
public class MyConfig {
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
return objectMapper;
}
}

Adding Hibernate4Module ruins serialization

I'm facing an issue after adding Hibernate4Module to support lazy-objects serialization.
My configuration file:
#EnableWebMvc
#ServletComponentScan(basePackages = "my.backend")
#EnableAutoConfiguration(exclude = MultipartAutoConfiguration.class)
#Configuration
#EnableSwagger2
public class MyConfiguration extends WebMvcConfigurerAdapter{
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false).
favorParameter(true).
parameterName("mediaType").
ignoreAcceptHeader(true).
useJaf(false).
defaultContentType(MediaType.APPLICATION_JSON).
mediaType("xml", MediaType.APPLICATION_XML).
mediaType("json", MediaType.APPLICATION_JSON);
}
/* Here we register the Hibernate4Module into an ObjectMapper, then set this custom-configured ObjectMapper
* to the MessageConverter and return it to be added to the HttpMessageConverters of our application*/
public MappingJackson2HttpMessageConverter jacksonMessageConverter(){
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper current = messageConverter.getObjectMapper();
//Registering Hibernate4Module to support lazy objects
current.registerModule(new Hibernate4Module());
messageConverter.setObjectMapper(current);
return messageConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//Here we add our custom-configured HttpMessageConverter
converters.add(jacksonMessageConverter());
super.configureMessageConverters(converters);
}
}
Hibernate entities get serializied ok.
The problem is with plain jsons, that where stored in DB or generated in any other way.
For example
String myJson = {"myField":"2123456"}
Is returned in mailformed format
"{\"myField\":"2123456"}
Looks like some default modules get broken. Can anyone give a piece of advice?

How to register Jackson Jdk8 module in Restlet

Is it possible to register the jackson-datatype-jdk8 module in the Restlet org.restlet.ext.jackson extension package? I need to take advantage of the new Optional feature. My guess is that it should be accessible through the converter services (getConverterService()) but I can't find anything in the documentation that suggests exactly how setting a module is possible.
I eventually pieced together from a variety of sources an answer that works with Restlet 2.3. My guess is that this will be refactored for Restlet 3 and it won't work for versions < 2.3 so be aware that this solution will most likely have a limited shelf life.
First step is to create a custom Jackson converter that implements any custom requirements you have:
public class CustomJacksonConverter extends JacksonConverter {
#Override
protected <T> JacksonRepresentation<T> create(MediaType mediaType, T source) {
ObjectMapper mapper = createMapper();
JacksonRepresentation<T> jr = new JacksonRepresentation<T>(mediaType, source);
jr.setObjectMapper(mapper);
return jr;
}
#Override
protected <T> JacksonRepresentation<T> create(Representation source, Class<T> objectClass) {
ObjectMapper mapper = createMapper();
JacksonRepresentation<T> jr = new JacksonRepresentation<T>(source, objectClass);
jr.setObjectMapper(mapper);
return jr;
}
private ObjectMapper createMapper() {
JsonFactory jsonFactory = new JsonFactory();
jsonFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
ObjectMapper mapper = new ObjectMapper(jsonFactory);
mapper.registerModule(new Jdk8Module());
return mapper;
}
}
You then need to create a way of replacing the default Jackson converter with a copy of your new one:
static void replaceConverter(
Class<? extends ConverterHelper> converterClass,
ConverterHelper newConverter) {
ConverterHelper oldConverter = null;
List<ConverterHelper> converters = Engine.getInstance().getRegisteredConverters();
for (ConverterHelper converter : converters) {
if (converter.getClass().equals(converterClass)) {
converters.remove(converter);
oldConverter = converter;
break;
}
}
converters.add(newConverter);
}
You can now replace the converter in your inbound root:
replaceConverter(JacksonConverter.class, new CustomJacksonConverter());
Great start by #tarka, but gave me stream errors; i propose some [lower impact] refinements:
public class CustomJacksonConverter extends JacksonConverter {
#Override
protected <T> JacksonRepresentation<T> create(MediaType mediaType, T source) {
return new CustomJacksonRepresentation<>(mediaType, source);
}
#Override
protected <T> JacksonRepresentation<T> create(Representation source, Class<T> objectClass) {
return new CustomJacksonRepresentation<>(source, objectClass);
}
}
and then...
public class CustomJacksonRepresentation<T> extends JacksonRepresentation<T> {
public CustomJacksonRepresentation(MediaType mediaType, T object) {
super(mediaType, object);
}
public CustomJacksonRepresentation(Representation representation, Class<T> objectClass) {
super(representation, objectClass);
}
public CustomJacksonRepresentation(T object) {
super(object);
}
#Override
protected ObjectMapper createObjectMapper() {
ObjectMapper mapper = super.createObjectMapper();
mapper.registerModule(new Jdk8Module());
return mapper;
}
}
and replace the converter in the same way #tarka does