After a lot of testing and research on this topic I cannot fully solve my issue. I'm using modelmapper for entity/DTO mapping in a springboot application. I'm trying to configure my modelmapper to map a Set to a simple DTO object. I already create the a custom converter and it is working as expected:
Converter<Set<CategoryTl>, CategoryTlDTO> converter = new AbstractCustomConverter<Set<CategoryTl>, CategoryTlDTO>() {
#Override
protected D convert(S source, MappingContext<Set<CategoryTl>, CategoryTlDTO> context) {
HashMap<String, CategoryTlDetailsDTO> map = new HashMap<>();
source.forEach(
categoryTl -> map.put(categoryTl.getCatalogLanguage().getLanguage().getCode(),
new CategoryTlDetailsDTO(categoryTl.getName(), categoryTl.getDescription()))
);
return new CategoryTlDTO(map);
}
};
My issue is now to apply this converter to all "Set => CategoryTlDTO". In nested object or not. I try to create a a new TypeMap but I cannot do it because of the "Set" collection.
mapper.createTypeMap(Set<CategoryTl>.class (-> not possible), CategoryTlDTO.class).setConverter(converter);
If I add the converter directly in the model mapper it is just not working.
mapper.addConverter(converter);
Do you have any hint or a solution for this? Maybe I miss something regarding TypeToken and TypeMap Inheritance.
Best regards,
I've not used ModelMapper, but the docs suggest you can use a TypeToken
Type setType = new TypeToken<Set<CategoryTl>>() {}.getType();
mapper.createTypeMap(setType, CategoryTlDTO.class).setConverter(converter);
Given the class:
class Container<T> {
T item;
String type;
Map<String,String> properties;
public void setItem(T _item) {
item = _item;
}
}
I have already the item serialized in a database as string with the name serialized. It is a Map<String,String>.
I don't know how to say Gson that this variable is already serialized.
So when I use Gson I first deserialize it, then serialize it back
Container<Map <String, String>> t = new Container<>(<other parameters>);
Map <String, String> m = gson.fromJson(serialized, new TypeToken<Map<String,String>>(){}.getType())
t.setItem(m);
gson.toJson(t, new TypeToken<Container<Map<String,String>>>() {}.getType());
This feels inefficient. How do I fix this?
I'm not sure that's possible. You're mixing object creation and serialization.
What you can do is create a new constructor with an additional String parameter and deserialize the string to get your item and set it automatically. That should be possible even with a parameterized type. That way you have 2 lines of code instead of 4.
I have a collection of classes, such as this:
entityClasses = new HashMap<String, Class>();
entityClasses.put("EntityType1", EntityType1.class);
entityClasses.put("EntityType2", EntityType2.class);
I also have a JSON list of their instances as well:
String entityJSON = "[{"type":"EntityType1","name":"... attributes"},...]";
Where the type attribute will determine the class of the object that will be the target of JSON parsing. How can I parse these using gson?
I tried with the following:
String type = "EntityType1"; // I already can fetch this.
final Class entityClass = entityClasses.get(type);
new Gson().fromJson(entityJSON, new TypeToken<ArrayList<entityClass>>(){}.getType());
Which would work if entityClass was an actual class name, and not a variable that represents a class. In my case however, I get the following error:
Unknown class: 'entityClass'
So how is it possible to parse by a Class variable?
Thanks in advance!
In general, you can't do that since you should pass a class as a generic type: not List<String.class>, but List<String>.
But you can use a workaround like this: store list TypeToken's instead of list generic types:
entityClasses = new HashMap<String, TypeToken>();
entityClasses.put("EntityType1", new TypeToken<ArrayList<EntityType1>>(){});
...
String type = "EntityType1"; // I already can fetch this.
final TypeToken typeToken= entityClasses.get(type);
new Gson().fromJson(entityJSON, typeToken.getType());
I map business objects to entities and there are cases where the structure of an entity is different from the business objects.
I have userCategories which are stored in the business object RecipeBo as strings, because the BO does not have to know anything about the internal structure of the entities. These strings need to be mapped to a relation of Recipe and RecipeUserCategoryRel, in addition to it, another field, userId of RecipeBo needs to be mapped in this RecipeUserCategoryRel too.
My approach (which works) is to create a wrapper and manually create the relations by hand, but this looks like tinkering:
public class BoMapper
{
private final static ModelMapper modelMapper = new ModelMapper();
static
{
modelMapper.addMappings(new IngredientMap());
}
public static void map(Object from, Object to)
{
modelMapper.map(from, to);
if (from instanceof RecipeBo && to instanceof Recipe)
{
RecipeBo recipeBo = (RecipeBo)from;
List<String> userCategories = recipeBo.getUserCategories();
List<RecipeUserCategoryRel> recipeUserCategoryRels = new ArrayList<>();
for (String userCategory : userCategories)
{
recipeUserCategoryRels.add(new RecipeUserCategoryRel(userCategory, recipeBo.getUserId()));
}
Recipe recipe = (Recipe)to;
recipe.setRecipeUserCategoryRels(recipeUserCategoryRels);
}
}
}
Is there a better approach of that what I'm doing in BoMapper, e.g. using converters or something? The difficulty is to map each element of the list and add the userId field too.
ISSUE
This is a complex situation because you are getting userId from other hierarchy and not directly from the List. ModelMapper would map List to List but if you don't configure ModelMapper as LOOSE it will not be able to work.
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.LOOSE);
Anyway, In case you configure ModelMapper in that manner (LOOSE mode) it would map the List and put in String property of your Class RecipeUserCategoryRel (in this case for example userCategory if it is a String and considering userId is not a String) the others (I'm not pretty sure) I think it would be null.
SOLUTION
Well, I think the solution to your issue is to create a Converter and add it to your ModelMapper instance:
RecipeBO (Source) -> Recipe (Destination)
The code would be as bellow:
ModelMapper mapper = new ModelMapper();
Converter<RecipeBO, Recipe> converter = new Converter<RecipeBO,
Recipe>() {
#Override
public Recipe convert(MappingContext<RecipeBO, Recipe> context) {
RecipeBO source = context.getSource();
Recipe destination = new Recipe();
List<String> userCategoryValues = source.getUserCategories();
List<RecipeUserCategoryRel> userCategoryToMap = new ArrayList<RecipeUserCategoryRel>();
for(final String userCategory : userCategoryValues){
userCategoryToMap.add(new RecipeUserCategoryRel(userCategory,source.getUserId()));
}
destination.setRecipeUserCategoryRels(userCategoryToMap);
//... Map other properties if you need
return destination;
}
};
//Option 1
mapper.createTypeMap(RecipeBO.class, Recipe.class).setConverter(converter);
//If you add as a converter directly also works (I don't know which one is better,
//choose above option (createTypeMap + setConverter) or the next (addConverter)
//Option 2 -> mapper.addConverter(converter);
I've tested and It works!!
If I had a Recipe as next:
RecipeBO recipe = new RecipeBO();
recipe.setUserId("1");
String values[] = new String[] { "abc", "klm", "xyz", "pqr" };
List<String> list = Arrays.asList(values);
recipe.setUserCategories(list);
And a RecipeBO:
Recipe recipe = new Recipe();
List<RecipeUserCategoryRel> recipes = new ArrayList<>();
recipes.add(new RecipeUserCategoryRel("abc", "1"));
recipes.add(new RecipeUserCategoryRel("klm", "1"));
recipes.add(new RecipeUserCategoryRel("xyz", "1"));
recipes.add(new RecipeUserCategoryRel("pqr", "1"));
recipe.setRecipeUserCategoryRels(recipes);
When I map RecipeBO to Recipe:
Recipe actual = mapper.map(getRecipeBO(), Recipe.class);
I get the next Output:
OUTPUT:
- RecipeUserCategoryRel(userCategory=abc, userId=1)
- RecipeUserCategoryRel(userCategory=klm, userId=1)
- RecipeUserCategoryRel(userCategory=xyz, userId=1)
- RecipeUserCategoryRel(userCategory=pqr, userId=1)
I have a Java project that currently returns a map to Camel without any nested key,value pairs, and the Jackson library marshalls it just fine into JSON using the Jackson library.
For example if I put the following two key,values into a demoMap:
Map<String,String> demoMap = new TreeMap<String,String>
demoMap.put("key1","5");
demoMap.put("key2","10");
I get the following JSON:
{"key1":"5","key2":"10"}
However, now some of my key,value entries will have an optional visibility that I need to put as a nested key value in my map. Any key,values that don't have an optional visibility will use the default. So, for example if I specify visibilities for key1, and key2, but not key3 I want to get JSON out that looks like this:
{"key1":"5",
"key2":"10",
"key3":"17",
"visibility" : { "key1": "a&b&!c", "key2": "a&b", "_default": "a" }
}
How can I get Camel to marshall a Java object with nested key,value pairs? I'm a visual learner, so a simple example would be helpful.
I tried changing my Map to have a value as an object i.e.,:
Map<String,Object> demoMap = new TreeMap<String,Object>
and then tried adding nested key,values for some keys with an ArrayList using http://examples.javacodegeeks.com/core-java/json/jackson/convert-java-map-to-from-json-using-jackson-example/ for reference, but realized that this just gives me a bunch of nested values under a key, not a bunch of nested key,value pairs.
Even when I tried it for grins, I got an error from the Camel processor with a java.lang.ClassCastException stating java.util.ArrayList cannot be cast to java.lang.String
And similarly when I tried to nest a Map inside my demoMap I got this ClassCastException:
3244 [hello.world.request.timer] ERROR org.apache.camel.processor.DefaultErrorHandler - Failed delivery for exchangeId: e6518e39-89b7-435e-96d9-ce26811ac67e. Exhausted after delivery attempt: 1 caught: java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
So I know how NOT to do it. :-/
I re-read the Camel JSON documentation at http://camel.apache.org/json.html but as of this writing it doesn't specify an example with nested key,value pairs.
UPDATE: Based on feedback from Tom I created two Maps i.e.,
Map<String,String> keyvalues = new TreeMap<String,String>();
Map<String,String> visibility = new TreeMap<String,String>();
Here is my class which I call SensorGenerator that loads a properties file into a Map:
package sample;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import org.codehaus.jackson.annotate.JsonAnyGetter;
import org.codehaus.jackson.annotate.JsonProperty;
public class SensorGenerator {
private Properties sourceProperties;
// create a map of sensor keyvalues, and a map of sensor visibility
Map<String,String> keyvalues = new TreeMap<String,String>();
#JsonProperty
Map<String,String> visibility = new TreeMap<String,String>();
#JsonAnyGetter
public Map<String, String> getKeyvalues() {
for (Object key : sourceProperties.keySet()) {
// Separate out each of the field:datatype:visibility tuples as an entry in the
// values array
String[] values = sourceProperties.getProperty((String) key).split(
",");
// split the key between 'sensor' and the 'number' Ex: sensor1 -> sensor,1
String[] keyArray = key.toString().split("(?<=([a-zA-Z]++))");
String keyNumber = keyArray[1]; // grab the number to append for each sensor
// define string buffer that appends sensor number for each sensor's
// keys. Ex: sensor1 would have s1make, s1makeDataType, etc.
StringBuffer sensorNumberStringBuffer = new StringBuffer();
sensorNumberStringBuffer.append("s");
sensorNumberStringBuffer.append(keyNumber);
// make, its data type, and visibility (with s# prefix)
StringBuffer makeStringBuffer = new StringBuffer();
makeStringBuffer.append(sensorNumberStringBuffer);
makeStringBuffer.append("make");
StringBuffer makeDataTypeStringBuffer = new StringBuffer();
makeDataTypeStringBuffer.append(sensorNumberStringBuffer);
makeDataTypeStringBuffer.append("makeDataType");
StringBuffer makeVizStringBuffer = new StringBuffer();
makeVizStringBuffer.append(sensorNumberStringBuffer);
makeVizStringBuffer.append("makeViz");
// model, its data type, and visibility (with s# prefix)
StringBuffer modelStringBuffer = new StringBuffer();
modelStringBuffer.append(sensorNumberStringBuffer);
modelStringBuffer.append("model");
StringBuffer modelDataTypeStringBuffer = new StringBuffer();
modelDataTypeStringBuffer.append(sensorNumberStringBuffer);
modelDataTypeStringBuffer.append("modelDataType");
StringBuffer modelVizStringBuffer = new StringBuffer();
modelVizStringBuffer.append(sensorNumberStringBuffer);
modelVizStringBuffer.append("modelViz");
// serialNumber, its data type, and visibility (with s# prefix)
StringBuffer serialNumberStringBuffer = new StringBuffer();
serialNumberStringBuffer.append(sensorNumberStringBuffer);
serialNumberStringBuffer.append("serialNumber");
StringBuffer serialNumberDataTypeStringBuffer = new StringBuffer();
serialNumberDataTypeStringBuffer.append(sensorNumberStringBuffer);
serialNumberDataTypeStringBuffer.append("serialNumberDataType");
StringBuffer serialNumberVizStringBuffer = new StringBuffer();
serialNumberVizStringBuffer.append(sensorNumberStringBuffer);
serialNumberVizStringBuffer.append("serialNumberViz");
// sensorType, its data type, and visibility (with s# prefix)
StringBuffer sensorTypeStringBuffer = new StringBuffer();
sensorTypeStringBuffer.append(sensorNumberStringBuffer);
sensorTypeStringBuffer.append("sensorType");
StringBuffer sensorTypeDataTypeStringBuffer = new StringBuffer();
sensorTypeDataTypeStringBuffer.append(sensorNumberStringBuffer);
sensorTypeDataTypeStringBuffer.append("sensorTypeDataType");
StringBuffer sensorTypeVizStringBuffer = new StringBuffer();
sensorTypeVizStringBuffer.append(sensorNumberStringBuffer);
sensorTypeVizStringBuffer.append("sensorTypeViz");
// put all the field:datatype keyvalues for this sensor in the keyvalues map
// and visibilities in the visibility map
// make, data type, and visibility
keyvalues.put(makeStringBuffer.toString(), values[0].split(":")[0]);
keyvalues.put(makeDataTypeStringBuffer.toString(), values[0].split(":")[1]);
visibility.put(makeVizStringBuffer.toString(), values[0].split(":")[2]);
// model, data type, and visibility
keyvalues.put(modelStringBuffer.toString(), values[1].split(":")[0]);
keyvalues.put(modelDataTypeStringBuffer.toString(), values[1].split(":")[1]);
visibility.put(modelVizStringBuffer.toString(), values[1].split(":")[2]);
// serialNumber, data type, and visibility
keyvalues.put(serialNumberStringBuffer.toString(), values[2].split(":")[0]);
keyvalues.put(serialNumberDataTypeStringBuffer.toString(), values[2].split(":")[1]);
visibility.put(serialNumberVizStringBuffer.toString(), values[2].split(":")[2]);
// sensorType, data type, and visibility
keyvalues.put(sensorTypeStringBuffer.toString(), values[3].split(":")[0]);
keyvalues.put(sensorTypeDataTypeStringBuffer.toString(), values[3].split(":")[1]);
visibility.put(sensorTypeVizStringBuffer.toString(), values[3].split(":")[2]);
// add in default visibility
visibility.put("_default", "a");
}
return keyvalues;
}
public void setSourceProperties(Properties properties) {
this.sourceProperties = properties;
}
}
Right now I just hardcoded the default visibility to "a", but will change that later to also be pulled from a properties file.
Your structure is a more than a map. It's two maps that are serialised differently. One way to represent this is:
public class Whatever{
Map<String,String> keyvalues;
Map<String,String> visibility;
}
What you'll end up with is this, which although represents the data is far from ideal:
{
"keyvalues" : { "key1": "5", "key2": "10", "key3": "17"},
"visibility" : { "key1": "a&b&!c", "key2": "a&b", "_default": "a" }
}
To get what you want, use #JsonAnyGetter. Something like this (it could be made much easier to use):
public class Whatever{
Map<String,String> keyvalues = new TreeMap<String,String>();
#JsonProperty
Map<String,String> visibility = new TreeMap<String,String>();
#JsonAnyGetter
public Map<String, String> getKeyvalues() {
return keyvalues;
}
}
which produces:
{"visibility":{"key1":"a&b&!c","key2":"a&b"},"key1":"5","key2":"10"}
I've been battling this today and your question inspired me to make it bloody work :D The annotations are here: https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations
See JUnit test here: https://gist.github.com/TomDemeranville/7009250