spring data mongodb enum mapping converter - spring

I would like code not throws exception when java code load enum value from mongo that not exists in enum code
Exemple :
java.lang.IllegalArgumentException: No enum constant fr.myapp.type.OrderOptionEnum.TELEPHONE
at java.lang.Enum.valueOf(Enum.java:238)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.getPotentiallyConvertedSimpleRead(MappingMongoConverter.java:819)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readCollectionOrArray(MappingMongoConverter.java:909)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readValue(MappingMongoConverter.java:1184)
Because TELEPHONE not existe in OrderOptionEnum
I juste want the code return null value
Any idea ?
Regards

you can add a custom converter implement Converter<String, OrderOptionEnum> there you implement your own convert logic from string to your enum.
something like this
public class OrderOptionEnumMongoConverter implements Converter<String, OrderOptionEnum> {
#Override
public OrderOptionEnum convert(String source) {
for (OrderOptionEnum OrderOptionEnum : OrderOptionEnum.values()) {
if (OrderOptionEnum.name().equals(source))
return OrderOptionEnum;
}
return null;
}
}
Notice !!! This converter will try to convert each string in mongo to your enum, thus may result in unwanted conversions, so make sure you do this only when needed.
you can add #ReadingConverter if you want this convert only when reading from mongo.

Related

How to link a Vaadin Grid with the result of Spring Mono WebClient data

This seems to be a missing part in the documentation of Vaadin...
I call an API to get data in my UI like this:
#Override
public URI getUri(String url, PageRequest page) {
return UriComponentsBuilder.fromUriString(url)
.queryParam("page", page.getPageNumber())
.queryParam("size", page.getPageSize())
.queryParam("sort", (page.getSort().isSorted() ? page.getSort() : ""))
.build()
.toUri();
}
#Override
public Mono<Page<SomeDto>> getDataByPage(PageRequest pageRequest) {
return webClient.get()
.uri(getUri(URL_API + "/page", pageRequest))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<>() {
});
}
In the Vaadin documentation (https://vaadin.com/docs/v10/flow/binding-data/tutorial-flow-data-provider), I found an example with DataProvider.fromCallbacks but this expects streams and that doesn't feel like the correct approach as I need to block on the requests to get the streams...
DataProvider<SomeDto, Void> lazyProvider = DataProvider.fromCallbacks(
q -> service.getData(PageRequest.of(q.getOffset(), q.getLimit())).block().stream(),
q -> service.getDataCount().block().intValue()
);
When trying this implementation, I get the following error:
org.springframework.core.codec.CodecException: Type definition error: [simple type, class org.springframework.data.domain.Page]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.data.domain.Page` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 1]
grid.setItems(lazyProvider);
I don't have experience with vaadin, so i'll talk about the deserialization problem.
Jackson needs a Creator when deserializing. That's either:
the default no-arg constructor
another constructor annotated with #JsonCreator
static factory method annotated with #JsonCreator
If we take a look at spring's implementations of Page - PageImpl and GeoPage, they have neither of those. So you have two options:
Write your custom deserializer and register it with the ObjectMapper instance
The deserializer:
public class PageDeserializer<T> extends StdDeserializer<Page<T>> {
public PageDeserializer() {
super(Page.class);
}
#Override
public Page<T> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
//TODO implement for your case
return null;
}
}
And registration:
SimpleModule module = new SimpleModule();
module.addDeserializer(Page.class, new PageDeserializer<>());
objectMapper.registerModule(module);
Make your own classes extending PageImpl, PageRequest, etc. and annotate their constructors with #JsonCreator and arguments with #JsonProperty.
Your page:
public class MyPage<T> extends PageImpl<T> {
#JsonCreator
public MyPage(#JsonProperty("content_prop_from_json") List<T> content, #JsonProperty("pageable_obj_from_json") MyPageable pageable, #JsonProperty("total_from_json") long total) {
super(content, pageable, total);
}
}
Your pageable:
public class MyPageable extends PageRequest {
#JsonCreator
public MyPageable(#JsonProperty("page_from_json") int page, #JsonProperty("size_from_json") int size, #JsonProperty("sort_object_from_json") Sort sort) {
super(page, size, sort);
}
}
Depending on your needs for Sort object, you might need to create MySort as well, or you can remove it from constructor and supply unsorted sort, for example, to the super constructor. If you are deserializing from input manually you need to provide type parameters like this:
JavaType javaType = TypeFactory.defaultInstance().constructParametricType(MyPage.class, MyModel.class);
Page<MyModel> deserialized = objectMapper.readValue(pageString, javaType);
If the input is from request body, for example, just declaring the generic type in the variable is enough for object mapper to pick it up.
#PostMapping("/deserialize")
public ResponseEntity<String> deserialize(#RequestBody MyPage<MyModel> page) {
return ResponseEntity.ok("OK");
}
Personally i would go for the second option, even though you have to create more classes, it spares the tediousness of extracting properties and creating instances manually when writing deserializers.
There are two parts to this question.
The first one is about asynchronously loading data for a DataProvider in Vaadin. This isn't supported since Vaadin has prioritized the typical case with fetching data straight through JDBC. This means that you end up blocking a thread while the data is loading. Vaadin 23 will add support for doing that blocking on a separate thread instead of keeping the UI thread blocked, but it will still be blocking.
The other half of your problem doesn't seem to be directly related to Vaadin. The exception message says that the Jackson instance used by the REST client isn't configured to support creating instances of org.springframework.data.domain.Page. I don't have direct experience with this part of the problem, so I cannot give any advice on exactly how to fix it.

How to write a custom convertor for recieving data annotated with #RequestBody

I'm trying to write a custom convertor for receiving data POSTed to a REST application. The object I want to populate already has its own builder that accepts a string JSON so I have to use that instead of the Jackson deserializer Spring would normally use.
I've tried a number of different things but I keep getting the following exception:
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class xxx.yyy.zzz.MyType]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `xxx.yyy.zzz.MyType` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (PushbackInputStream); line: 1, column: 1]
at at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:281) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:250) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
My convertor looks like:
public class MyConverter extends AbstractHttpMessageConverter<MyType> {
public MyConverter() {
super(/*MediaType.TEXT_PLAIN*/ MediaType.APPLICATION_JSON);
}
#Override
protected boolean supports(Class<?> type) {
return MyType.class.isAssignableFrom(type);
}
#Override
protected MyType readInternal(Class<? extends MyType> type, HttpInputMessage inputMessage) throws IOException {
String str = ..... read data from inputMessage
return MyType.build(str);
}
#Override
protected void writeInternal(MyType s, HttpOutputMessage outputMessage) {
}
}
and the controller is:
#RequestMapping(method = RequestMethod.POST)
public void add(#RequestBody MyType data) {
System.out.println("add:" + data.toString());
}
Even if change the MediaType in the constructor for MyConverter to 'MediaType.ALL' it will still fail. Curiously if I change it to TEXT_PLAIN and POST with Content-Type set to 'text/plain' it works!
Answering my own question. The problem turned out to be the order the HttpMessageConverter objects are processed. The inbuilt converters were being processed, and failing, before my converter had chance.
This isn't the best code, but it works:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void extendMessageConverters(#Nonnull List<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> temp = new ArrayList<>();
temp.add(new MyConverter());
temp.addAll(converters);
converters.clear();
converters.addAll(temp);
}
}
I can't quite believe this is the best answer for what, I think, should be a fairly standard problem. If somebody can suggest a better answer I'll happily accept that instead

Spring Data Neo4j: Converter of object to string works, but object to long is not executed

I have a really strange issue with converting from domain objects to those Neo4j can natively store as property value. As a test case I use Joda's DateTime. A object of that type can be converted to a String or Long quite easily.
The conversion from DateTime to String works flawlessly with this code:
public class DateTimeToStringConverter implements Converter<DateTime, String> {
#Override
public String convert(DateTime source) {
return source.toDateTimeISO().toString();
}
}
The property shows up in the node:
Node[1] {
'__type__' = '...',
'entityEditedAt' = '2012-12-28T12:32:50.308+01:00',
'entityCreatedAt' = '2012-12-28T12:32:50.297+01:00',
...
}
However if I like to save the DateTime as Long (useful to sort by time in Cypher), it does not work at all. Here is my converter:
public class DateTimeToLongConverter implements Converter<DateTime, Long> {
#Override
public Long convert(DateTime source) {
return source.toDateTimeISO().getMillis();
}
}
The property is not saved on the node. Thus it is missing completely. No exception is thrown. It seems like the conversion code is not called at all.
The converters are hooked to Spring Data using code based configuration:
#Bean
public ConversionServiceFactoryBean conversionService() {
Set converters = Sets.newHashSet();
// These work!
converters.add(new DateTimeToStringConverter());
converters.add(new StringToDateTimeConverter());
// These don't :-(
//converters.add(new DateTimeToLongConverter());
//converters.add(new LongToDateTimeConverter());
ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
bean.setConverters(converters);
return bean;
}
Any clues? I'm quite lost here, as it should work in my opinion...
Edit
I found following text in the Spring Data Neo4j documentation:
All fields convertible to a String using the Spring conversion services will be stored as a string.
Does this mean, that only conversions to string are supported? This seems rather limiting.
Tell SDN that you want to store your joda DateTime property as a long with:
#NodeEntity
public class MyEntity {
...
#GraphProperty(propertyType = Long.class)
private DateTime timestamp;
....
}
Then your registered DateTimeToLongConverter will kick in.

How do you handle deserializing empty string into an Enum?

I am trying to submit a form from Ext JS 4 to a Spring 3 Controller using JSON. I am using Jackson 1.9.8 for the serialization/deserialization using Spring's built-in Jackson JSON support.
I have a status field that is initially null in the Domain object for a new record. When the form is submitted it generates the following json (scaled down to a few fields)
{"id":0,"name":"someName","status":""}
After submitted the following is seen in the server log
"nested exception is org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.blah.domain.StatusEnum from String value '': value not one of the declared Enum instance names"
So it appears that Jackson is expecting a valid Enum value or no value at all including an empty string. How do I fix this whether it is in Ext JS, Jackson or Spring?
I tried to create my own ObjectMapper such as
public class MyObjectMapper extends Object Mapper {
public MyObjectMapper() {
configure(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}
}
and send this as a property to MappingJacksonMappingView but this didn't work. I also tried sending it in to MappingJacksonHttpMessageConverter but that didn't work. Side question: Which one should I be sending in my own ObjectMapper?
Suggestions?
The other thing you could do is create a specialized deserializer (extends org.codehaus.jackson.map.JsonDeserializer) for your particular enum, that has default values for things that don't match. What I've done is to create an abstract deserializer for enums that takes the class it deserializes, and it speeds this process along when I run into the issue.
public abstract class EnumDeserializer<T extends Enum<T>> extends JsonDeserializer<T> {
private Class<T> enumClass;
public EnumDeserializer(final Class<T> iEnumClass) {
super();
enumClass = iEnumClass;
}
#Override
public T deserialize(final JsonParser jp,
final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final String value = jp.getText();
for (final T enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
return enumValue;
}
}
return null;
}
}
That's the generic class, basically just takes an enum class, iterates over the values of the enum and checks the next token to match any name. If they do it returns it otherwise return null;
Then If you have an enum MyEnum you'd make a subclass of EnumDeserializer like this:
public class MyEnumDeserializer extends EnumDeserializer<MyEnum> {
public MyEnumDeserializer() {
super(MyEnum.class);
}
}
Then wherever you declare MyEnum:
#JsonDeserialize(using = MyEnumDeserializer.class)
public enum MyEnum {
...
}
I'm not familiar with Spring, but just in case, it may be easier to handle that on the client side:
Ext.define('My.form.Field', {
extend: 'Ext.form.field.Text',
getSubmitValue: function() {
var me = this,
value;
value = me.getRawValue();
if ( value === '' ) {
return ...;
}
}
});
You can also disallow submitting empty fields by setting their allowBlank property to false.
Ended up adding defaults in the EXT JS Model so there is always a value. Was hoping that I didn't have to this but it's not that big of a deal.
I have the same issue. I am reading a JSON stream with some empty strings. I am not in control of the JSON stream, because it is from a foreign service. And I am always getting the same error message. I tried this here:
mapper.getDeserializationConfig().with(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
But without any effect. Looks like a Bug.

Convert IQueryable<T> to IQueryable<Y>

I'm try to do this : I'm using EF code first to map an old existing database. There's many fields with the wrong type (ex: char(1) used as boolean) so I've created a wrapper class for my db context that maps perfectly to the database table. Now, I want to expose an IQueryable of my Entity type on my repository. See my example :
public class MyContext:DbContext
{
public DbSet<WrappedEntity> WrapperEntities;
}
public class Repository
{
private MyContext _context;
//contructors omitted
public IQueryable<Entity> GetEntities()
{
return _context.WrapperEntities; //doesn't compile, I need some convertion here
}
}
I already have my convertion routine, the only thing missing is a way to query my DbContext thought my repository without exposing the WrappedEntity class, Is it possible and how ?
Thanks.
Usually you project with Queryable.Select to change the type of your query...
public IQueryable<Entity> GetEntities()
{
return _context.WrapperEntities.Select(x => new Entity(){...});
}

Resources