How to convert a Object with enum attribute in Spring Data MongoDB? - spring

I have the following class that I want to save and query from MongoDB. I managed to set a custom convert that converts from InstanceType to String and String to InstanceType. InstanceType is a custom enum that will be stored in a different way!
#Document
public class Instance {
private String name;
private InstanceType type;
private List<Configuration> configurations;
private Map<String, String> properties;
}
MongoConfiguration class
#Bean
#Override
public CustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new InstanceTypeToStringConverter());
converters.add(new StringToInstanceTypeConverter());
return new CustomConversions(converters);
}
#Bean
#Override
public MappingMongoConverter mappingMongoConverter() throws Exception {
MongoMappingContext mappingContext = new MongoMappingContext();
DbRefResolver databaseResolver = new DefaultDbRefResolver(mongoFactory());
MappingMongoConverter mongoConverter = new MappingMongoConverter(databaseResolver, mappingContext);
mongoConverter.setCustomConversions(customConversions());
mongoConverter.afterPropertiesSet();
return mongoConverter;
}
The problem I'm having is that when I try to query the Instance object in MongoDB, Spring uses StringToInstanceType to convert "name" attribute. Why Spring is using this converter? This converter should not only be applied to "type" attribute?
This is the query I'm trying to execute:
public Instance getInstance(InstanceType type, String name) {
return mongoTemplate.findOne(Query.query(
Criteria.where("type").is(type)
.and("name").is(name)
), Instance.class, COLLECTION_INSTANCES);
}
Type attribute is converted using InstanceTypeToStringConverter (Thats OK because the source is InstanceType and target is String)
Name attribute is converted using StringToInstanceTypeConverter (It's wrong because the source is String and target is a String)
Because os this behavior, the query is returning NULL.
Let's say I have the follow object in MongoDB:
{"type": "user", "name": "Sandro"}
InstanceType.USER is converted to "user" (InstanceTypeToStringConverter).
"Sandro" is converted to null (StringToInstanceTypeConverter) because InstanceType.getInstanceType("Sandro") returns null.
In this case Spring will query mongo this way:
Where type = "user" and name = null

Related

Spring data elasticsearch embedded field mapping

I'm struggling with mappings some field. I looked for an answer but couldn't find anything solving my case. Let's cut to the chase.
I have my document class
#Doucment
public class DocumentClass {
#Field(type = FieldType.Nested)
private EmployeeId employeeId;
}
An EmployeeId is wrapper for my uuid identifier. This object has nothing but just getters and setters and jackson annotations. The thing is that object extends some base class so such objects like EmployeeId can inherit this object. This super class has field id and this causes the problem. When I post some data to elasticsearch then it looks like this:
{
"employeeId": {
"id": "someUUID"
}
}
But I want to map this to be like:
{
"employeeId": "someUUID"
}
I wonder if there is a way to flatten this object.
If I get it right, you want to convert your EmployeeId class to a String and back. You have 2 possibilities to do that:
Using a property converter
If you only want to convert an EmployeeId in this entity and might keep it as it is in another, you should use a property converter that is only registered for this property:
import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter;
public class EmployeeIdConverter implements PropertyValueConverter {
#Override
public Object write(Object value) {
return value instanceof EmployeeId employeeId ? employeeId.getId() : value.toString();
}
#Override
public Object read(Object value) {
return new EmployeeId(value.toString());
}
}
This converter must be registered on the property, notice that the field type is set to Keyword as it probably should not be analysed:
import org.springframework.data.elasticsearch.annotations.ValueConverter;
#Document
public class DocumentClass {
#Field(type = FieldType.Keyword)
#ValueConverter(EmployeeIdConverter.class)
private EmployeeId employeeId;
}
Using a global converter
If you are using this EmployeeId at several places you might want register globally 2 converters for the two conversion directions:
#WritingConverter
public class EmployeeIdToString implements Converter<EmployeeId, String>{
#Nullable
#Override
public String convert(EmployeeId employeeId) {
return employeeId.getId();
}
}
#ReadingConverter
public class StringToEmployeeId implements Converter<String, EmployeeId>{
#Nullable
#Override
public EmployeeId convert(String id) {
return new EmployeeId(id);
}
}
To register these, you need to provide a custom client configuration (see the documentation):
#Configuration
public class MyClientConfig extends ElasticsearchConfiguration {
#Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
}
#Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
Collection<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new EmployeeIdToString());
converters.add(new StringToEmployeeId());
return new ElasticsearchCustomConversions(converters);
}
}
In this case, only the field type needs to be adjusted
#Document
public class DocumentClass {
#Field(type = FieldType.Keyword)
private EmployeeId employeeId;
}
I am guessing you have an Id field which is a string.
You need to put a #JsonValue annotation on that field to make jackson serialize it the way you want.
Field annotated by JsonValue will be used to serialize your pojo into json.
If your Id field is private, then add the annotation on the getter of that field.
https://fasterxml.github.io/jackson-annotations/javadoc/2.8/com/fasterxml/jackson/annotation/JsonValue.html

Is there a way to configure LocalDate format for serializing and deserializing in the whole spring application?

I have the following problem I hope someone can give me a hand:
Context: 3 Rest endpoints
Create (register)
Find (findKid)
Report (listDashboardInfo)
Requirement: Use the same date format yyyyMMdd for LocalDates in the whole application
Problem: Using #DateTimeFormat(pattern = DateUtils.SHORT_DATE_PATTERN) works for register and listDashboardInfo but not for findKid
These are the relevant parts of the code:
BODY
{
"sailDate": "20191201"
}
#PostMapping(KID_PATH)
#ResponseStatus(HttpStatus.CREATED)
public KidDTO register(#RequestBody #Valid KidDTO kid) {
return kidService.saveKid(kid);
}
GET /kid/0001::20190901
RESPONSE
{
"sailDate": "2019-09-01"
}
#GetMapping(KID_FIND_PATH)
public CompletableFuture<KidDTO> findKid(#PathVariable String id) {
return kidService.findKid(id);
}
GET /kid?shipCode=AL&sailDate=20190901
#GetMapping(KID_LIST_PATH)
public CompletableFuture<Slice<DashboardDTO>> listDashboardInfo(#Valid DashboardFilter filter, Pageable pageable) {
return kidService.listKidsWithStatistics(filter, pageable);
}
#Getter
#Setter
public class DashboardFilter {
#NotNull
#DateTimeFormat(pattern = DateUtils.SHORT_DATE_PATTERN)
private LocalDate sailDate;
}
#Data
public class KidDTO {
#NotNull
#DateTimeFormat(pattern = DateUtils.SHORT_DATE_PATTERN)
private LocalDate sailDate;
}
Tests I did:
spring.jackson.date-format in application.properties: From https://blog.codecentric.de/en/2017/08/parsing-of-localdate-query-parameters-in-spring-boot/ this just apply for Date not LocalDate.
Using #JsonFormat(pattern = DateUtils.SHORT_DATE_PATTERN) the listDashboardInfo doesn't recognize the format and generates error
From stackoverflow I also found Spring doesn't use Jackson to deserialize query params so:
- I created a #ControllerAdvice with #InitBinder but the method setAsText is never called:
#ControllerAdvice
public class GlobalDateBinder {
#InitBinder
public void binder(WebDataBinder binder) {
binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
#Override
public void setAsText(String text) throws IllegalArgumentException {
LocalDate.parse(text, DateUtils.SHORT_DATE_FORMATTER);
}
});
}
}
Also I tried with a #Bean public Formatter<LocalDate> localDateFormatter() but nothing change:
#Bean
public FormattingConversionService conversionService() {
DefaultFormattingConversionService conversionService =
new DefaultFormattingConversionService(false);
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateFormatter(DateUtils.SHORT_DATE_FORMATTER);
registrar.registerFormatters(conversionService);
return conversionService;
}
#Bean
public Formatter<LocalDate> localDateFormatter() {
return new Formatter<LocalDate>() {
#Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, DateUtils.SHORT_DATE_FORMATTER);
}
#Override
public String print(LocalDate object, Locale locale) {
return DateUtils.SHORT_DATE_FORMATTER.format(object);
}
};
}
Any one has an idea of what is happening?
how to make the response of findKid be formatted?
How to configure the whole application with the same date format to works in serialization and parsing/deserializing processes?
UPDATE:
I found here https://stackoverflow.com/questions/30871255/spring-boot-localdate-field-serialization-and-deserialization that I can use #JsonFormat for rest controllers (serialize and deserialize) and #DateTimeFormat for ModelView controllers but using both, at the same time, fixed my error so I don't understand why is that behavior if I only have rest controllers. Looks like in my case #DateTimeFormat deserialize and #JsonFormat serialize, is that the expected behavior? Is there any misconfiguration?
you can add this bean to you configuration:
#Bean
public ObjectMapper objectMapper() {
DateTimeFormatter dateFormatter; // create your date formatter
DateTimeFormatter dateTimeFormatter; // create your date and time formatter
ObjectMapper mapper = new ObjectMapper();
SimpleModule localDateModule = new SimpleModule();
localDateModule.addDeserializer(LocalDate.class,
new LocalDateDeserializer(formatter));
localDateModule.addSerializer(LocalDate.class,
new LocalDateSerializer(formatter));
localDateModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(dateTimeFormatter));
localDateModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(dateTimeFormatter));
mapper.registerModules(localDateModule);
return mapper;
}
Just set the property spring.jackson.date-format to any format you want inside you application.properties or application.yml.
Example with application.properties:
spring.jackson.date-format=yyyyMMdd
Example with application.yml:
spring:
jackson:
date-format: yyyyMMdd
Source and other available properties: https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

Spring Data Mongodb: json string to BasicDBObject

I've created this custom converter:
#Component
#WritingConverter
public class MetadataWriterConverter implements Converter<Metadata, DBObject> {
#Override
public DBObject convert(Metadata metadata) {
DBObject dbObject = new BasicDBObject();
dbObject.put("name", metadata.getName());
dbObject.put("metadata", (BasicDBObject) BasicDBObject.parse(reference.getMetadata()));
dbObject.removeField("_class");
return dbObject;
}
}
I'm getting this exception:
Caused by: org.bson.BsonInvalidOperationException: readStartDocument can only be called when CurrentBSONType is DOCUMENT, not when CurrentBSONType is ARRAY.
The problem is on:
(BasicDBObject) BasicDBObject.parse(metadata.getMetadata())
the content of metadata.getMetadata is: "[{'departament': 'JUST'}]".
Metadata class is:
public class Metadata {
private String id;
private String user;
private String metadata;
}
The content of metadata field is a json string, I'm trying to convert to BasicDbObject, but the problem appears when this string is an json array: [{},{}].
Guess:
Metadata met = new Metadata();
met.setId("Mdt1");
met.setUser("user");
met.setMetadata("[{'departament': 'JUST'}]");
What I want to get is:
{
"id": Mdt1,
"user": "user",
"metadata": [{"departament": "JUST"}]
}
Any ideas about how to refactor my converter?
Actually, BasicDBObject.parse() expects a JSONObject instead of a JSONArray that you are passing in your example. Check the docs here - http://api.mongodb.com/java/current/com/mongodb/BasicDBObject.html#parse-java.lang.String-
Instead, you can try converting your reference.getMetadata() into a valid JSON String and then using BasicDBList for your JSONArray. Something like below:
#Component
#WritingConverter
public class MetadataWriterConverter implements Converter<Metadata, DBObject>
{
#Override
public DBObject convert(Metadata metadata) {
DBObject dbObject = new BasicDBObject();
dbObject.put("name", metadata.getName());
String jsonString = String.format("{\"data\": " + reference.getMetadata() + "}");
BasicDBObject basicDBObject = (BasicDBObject) BasicDBObject.parse(jsonString);
BasicDBList parsedList = (BasicDBList) basicDBObject.get("data");
dbObject.put("metadata", parsedList);
dbObject.removeField("_class");
return dbObject;
}
}

Cannot use Map as a JSON #RequestParam in Spring REST controller

This controller
#GetMapping("temp")
public String temp(#RequestParam(value = "foo") int foo,
#RequestParam(value = "bar") Map<String, String> bar) {
return "Hello";
}
Produces the following error:
{
"exception": "org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException",
"message": "Failed to convert value of type 'java.lang.String' to required type 'java.util.Map'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Map': no matching editors or conversion strategy found"
}
What I want is to pass some JSON with bar parameter:
http://localhost:8089/temp?foo=7&bar=%7B%22a%22%3A%22b%22%7D, where foo is 7 and bar is {"a":"b"}
Why is Spring not able to do this simple conversion? Note that it works if the map is used as a #RequestBody of a POST request.
Here is the solution that worked:
Just define a custom converter from String to Map as a #Component. Then it will be registered automatically:
#Component
public class StringToMapConverter implements Converter<String, Map<String, String>> {
#Override
public Map<String, Object> convert(String source) {
try {
return new ObjectMapper().readValue(source, new TypeReference<Map<String, String>>() {});
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
}
If you want to use Map<String, String> you have to do the following:
#GetMapping("temp")
public String temp(#RequestParam Map<String, String> blah) {
System.out.println(blah.get("a"));
return "Hello";
}
And the URL for this is: http://localhost:8080/temp?a=b
With Map<String, String>you will have access to all your URL provided Request Params, so you can add ?c=d and access the value in your controller with blah.get("c");
For more information have a look at: http://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/spring-mvc-request-param/ at section Using Map with #RequestParam for multiple params
Update 1: If you want to pass a JSON as String you can try the following:
If you want to map the JSON you need to define a corresponding Java Object, so for your example try it with the entity:
public class YourObject {
private String a;
// getter, setter and NoArgsConstructor
}
Then make use of Jackson's ObjectMapper to map the JSON string to a Java entity:
#GetMapping("temp")
public String temp(#RequestParam Map<String, String> blah) {
YourObject yourObject =
new ObjectMapper().readValue(blah.get("bar"),
YourObject.class);
return "Hello";
}
For further information/different approaches have a look at: JSON parameter in spring MVC controller

How to set different type for variable in POJO than expected while deserializing json using gson in android(see example)

Bear with my English. I have a simple json,
{
"Hint2": "L"
}
this is the POJO that works.
public class Hints {
#SerializedName("Hint2")
#Expose
private String Hint2;
public void setHint1(Object Hint2) {
this.Hint2 = (Hint2);
}
}
i want to change it to
public class Hints {
#SerializedName("Hint2")
#Expose
public final ObservableField<String> Hint2 = new ObservableField<>();
public void setHint2(String Hint2) {
this.Hint2.set(Hint2);
}
}
both class has same setter method, same #SerializedName annotation tag. only type of Hint2 object is changed. but the latter one throws exception shown below
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at..
so i believe deserialization depends on what kind of variable "Hint2" is.
Is there a way to make it work with ObservableField rather than using String?
The reason i'm trying this is android binding library, which supports binding objects directly to xml files. and the ObservableField automatically updates UI when corresponding value in POJO is changed.
Update:
gson design document has this
Using fields vs getters to indicate Json elements
Some Json libraries use the getters of a type to deduce the Json elements. We chose to use all fields (up the inheritance hierarchy) that are not transient, static, or synthetic. We did this because not all classes are written with suitably named getters. Moreover, getXXX or isXXX might be semantic rather than indicating properties.
However, there are good arguments to support properties as well. We intend to enhance Gson in a latter version to support properties as an alternate mapping for indicating Json fields. For now, Gson is fields-based.
so this indicates that Gson is fields-based. this pretty much answers my question but still waiting if anyone has someway around this.
I came across the same requirements, and resolved it finally, here are the steps:
create the class GsonUtils:
public class GsonUtils {
// code following
//...
}
following code are in this class
write a customized serializer & deserializer:
private static class ObservableFieldSerializerDeserializer implements JsonSerializer>, JsonDeserializer> {
#Override
public JsonElement serialize(ObservableField src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.get());
}
#Override
public ObservableField deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
final Type type = ((ParameterizedType) typeOfT).getActualTypeArguments()[0];
return new ObservableField((T) GsonUtils.getGson().fromJson(json, type));
}
}
you need to register ObservableField types to Gson:
private static GsonBuilder createGsonBuilder() {
final GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(new TypeToken&ltObservableField&ltString&gt&gt(){}.getType(), new ObservableFieldSerializerDeserializer&ltString&gt());
...// register more types which are wrapped by ObservableFields
return gsonBuilder;
}
create the Gson which is used by the deserializer
private static final Gson sGson = createGson();
private static Gson createGson() {
return createGsonBuilder().create();
}
// this is used by the deserializer
public static Gson getGson() {
return sGson;
}
that's all, hope it helps
I just ran into what I think is the same issue, and here is a JUnit4 test showing how I solved it with Jackson for a POJO, but of course String would work as well.
public class ObservableDeserializationTest {
private static class ObservableDeserializer extends JsonDeserializer<ObservableField> implements ContextualDeserializer {
private Class<?> mTargetClass;
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
mTargetClass = property.getType().containedType(0).getRawClass();
return this;
}
#Override
public ObservableField deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObservableField result = new ObservableField();
result.set(p.readValueAs(mTargetClass));
return result;
}
}
private static class SomePojo {
public String id;
public String name;
}
private static class ObservableTestClass {
#JsonDeserialize(using = ObservableDeserializer.class)
public ObservableField<SomePojo> testObj = new ObservableField<>();
}
#Test
public void DeserializingAnObservableObjectShouldSetValueCorrectly() {
ObservableTestClass tc = null;
try {
tc = new ObjectMapper().readValue("{\"testObj\":{\"name\":\"TestName\",\"id\":\"TestId\"}}", ObservableTestClass.class);
} catch (IOException e) {
e.printStackTrace();
}
Assert.assertEquals("TestName", tc.testObj.get().name);
Assert.assertEquals("TestId", tc.testObj.get().id);
}
}
The key is the ContextualDeserializer interface that allows extracting the contained class type. Jackson provides several options for registering a custom deserializer, so this is but one way of doing it. Also, it would probably be a good idea to override getNullValue as well in the deserializer if you would use this for real.

Resources