I have the following Enum Class:
#XmlJavaTypeAdapter(value = PastMedicalHistoryAdapter.class)
public enum PastMedicalHistory {
Diabetes, Obesity, Smoking, COPD, CAD, PVD, Other
}
and Generic Adapter:
public abstract class GenericEnumAdapter<T extends Enum> extends XmlAdapter<String, Enum> {
#Override
public Enum unmarshal(String v) throws Exception {
log.info("unmarshal: {}", v);
return convert(v + "");
}
public abstract T convert(String value);
#Override
public String marshal(Enum v) throws Exception {
log.info("marshal: {}", v.name());
String s = "{\"" + v.name() + "\":" + true + "}";
return s;
}
}
and basic implementation with
public class PastMedicalHistoryAdapter extends GenericEnumAdapter<PastMedicalHistory> {
#Override
public PastMedicalHistory convert(String value) {
return PastMedicalHistory.valueOf(value);
}
}
and I used it like this:
#Data
#XmlRootElement(name = "Patient")
public class Test {
private List<PastMedicalHistory> history;
public static void main(String[] args) throws Exception {
JAXBContext cxt = JAXBContext.newInstance(Test.class);
Marshaller mar = cxt.createMarshaller();
mar.setProperty(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
mar.setProperty(JAXBContextProperties.MEDIA_TYPE, "application/json");
mar.setProperty(JAXBContextProperties.JSON_INCLUDE_ROOT, Boolean.FALSE);
Test t = new Test();
t.setHistory(Arrays.asList(PastMedicalHistory.CAD, PastMedicalHistory.Diabetes));
mar.marshal(t, System.out);
}
}
the problem, is Output for history is always null like this:
[exec:exec]
2013-09-29 12:13:18:511 INFO marshal: CAD
2013-09-29 12:13:18:522 INFO marshal: Diabetes
{
"history" : [ null, null ]
}
I'm using Moxy 2.5.1 as JAXB Provider, so What I'm missing, or what I'm doing wrong?
Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB (JSR-222) expert group.
I have been able to reproduce the bug that you are hitting. You can use the following link to track our progress on this issue:
http://bugs.eclipse.org/418486
Related
How can I deserialize JSON string that contains enum values that are case insensitive? (using Jackson Databind)
The JSON string:
[{"url": "foo", "type": "json"}]
and my Java POJO:
public static class Endpoint {
public enum DataType {
JSON, HTML
}
public String url;
public DataType type;
public Endpoint() {
}
}
in this case,deserializing the JSON with "type":"json" would fail where as "type":"JSON" would work.
But I want "json" to work as well for naming convention reasons.
Serializing the POJO also results in upper case "type":"JSON"
I thought of using #JsonCreator and #JsonGetter:
#JsonCreator
private Endpoint(#JsonProperty("name") String url, #JsonProperty("type") String type) {
this.url = url;
this.type = DataType.valueOf(type.toUpperCase());
}
//....
#JsonGetter
private String getType() {
return type.name().toLowerCase();
}
And it worked. But I was wondering whether there's a better solutuon because this looks like a hack to me.
I can also write a custom deserializer but I got many different POJOs that use enums and it would be hard to maintain.
Can anyone suggest a better way to serialize and deserialize enums with proper naming convention?
I don't want my enums in java to be lowercase!
Here is some test code that I used:
String data = "[{\"url\":\"foo\", \"type\":\"json\"}]";
Endpoint[] arr = new ObjectMapper().readValue(data, Endpoint[].class);
System.out.println("POJO[]->" + Arrays.toString(arr));
System.out.println("JSON ->" + new ObjectMapper().writeValueAsString(arr));
Jackson 2.9
This is now very simple, using jackson-databind 2.9.0 and above
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
// objectMapper now deserializes enums in a case-insensitive manner
Full example with tests
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
private enum TestEnum { ONE }
private static class TestObject { public TestEnum testEnum; }
public static void main (String[] args) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
try {
TestObject uppercase =
objectMapper.readValue("{ \"testEnum\": \"ONE\" }", TestObject.class);
TestObject lowercase =
objectMapper.readValue("{ \"testEnum\": \"one\" }", TestObject.class);
TestObject mixedcase =
objectMapper.readValue("{ \"testEnum\": \"oNe\" }", TestObject.class);
if (uppercase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize uppercase value");
if (lowercase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize lowercase value");
if (mixedcase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize mixedcase value");
System.out.println("Success: all deserializations worked");
} catch (Exception e) {
e.printStackTrace();
}
}
}
I ran into this same issue in my project, we decided to build our enums with a string key and use #JsonValue and a static constructor for serialization and deserialization respectively.
public enum DataType {
JSON("json"),
HTML("html");
private String key;
DataType(String key) {
this.key = key;
}
#JsonCreator
public static DataType fromString(String key) {
return key == null
? null
: DataType.valueOf(key.toUpperCase());
}
#JsonValue
public String getKey() {
return key;
}
}
Since Jackson 2.6, you can simply do this:
public enum DataType {
#JsonProperty("json")
JSON,
#JsonProperty("html")
HTML
}
For a full example, see this gist.
In version 2.4.0 you can register a custom serializer for all the Enum types (link to the github issue). Also you can replace the standard Enum deserializer on your own that will be aware about the Enum type. Here is an example:
public class JacksonEnum {
public static enum DataType {
JSON, HTML
}
public static void main(String[] args) throws IOException {
List<DataType> types = Arrays.asList(JSON, HTML);
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<Enum> modifyEnumDeserializer(DeserializationConfig config,
final JavaType type,
BeanDescription beanDesc,
final JsonDeserializer<?> deserializer) {
return new JsonDeserializer<Enum>() {
#Override
public Enum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
Class<? extends Enum> rawClass = (Class<Enum<?>>) type.getRawClass();
return Enum.valueOf(rawClass, jp.getValueAsString().toUpperCase());
}
};
}
});
module.addSerializer(Enum.class, new StdSerializer<Enum>(Enum.class) {
#Override
public void serialize(Enum value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(value.name().toLowerCase());
}
});
mapper.registerModule(module);
String json = mapper.writeValueAsString(types);
System.out.println(json);
List<DataType> types2 = mapper.readValue(json, new TypeReference<List<DataType>>() {});
System.out.println(types2);
}
}
Output:
["json","html"]
[JSON, HTML]
If you're using Spring Boot 2.1.x with Jackson 2.9 you can simply use this application property:
spring.jackson.mapper.accept-case-insensitive-enums=true
I went for the solution of Sam B. but a simpler variant.
public enum Type {
PIZZA, APPLE, PEAR, SOUP;
#JsonCreator
public static Type fromString(String key) {
for(Type type : Type.values()) {
if(type.name().equalsIgnoreCase(key)) {
return type;
}
}
return null;
}
}
For those who tries to deserialize Enum ignoring case in GET parameters, enabling ACCEPT_CASE_INSENSITIVE_ENUMS will not do any good. It won't help because this option only works for body deserialization. Instead try this:
public class StringToEnumConverter implements Converter<String, Modes> {
#Override
public Modes convert(String from) {
return Modes.valueOf(from.toUpperCase());
}
}
and then
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToEnumConverter());
}
}
The answer and code samples are from here
To allow case insensitive deserialization of enums in jackson, simply add the below property to the application.properties file of your spring boot project.
spring.jackson.mapper.accept-case-insensitive-enums=true
If you have the yaml version of properties file, add below property to your application.yml file.
spring:
jackson:
mapper:
accept-case-insensitive-enums: true
With apologies to #Konstantin Zyubin, his answer was close to what I needed - but I didn't understand it, so here's how I think it should go:
If you want to deserialize one enum type as case insensitive - i.e. you don't want to, or can't, modify the behavior of the entire application, you can create a custom deserializer just for one type - by sub-classing StdConverter and force Jackson to use it only on the relevant fields using the JsonDeserialize annotation.
Example:
public class ColorHolder {
public enum Color {
RED, GREEN, BLUE
}
public static final class ColorParser extends StdConverter<String, Color> {
#Override
public Color convert(String value) {
return Arrays.stream(Color.values())
.filter(e -> e.getName().equalsIgnoreCase(value.trim()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid value '" + value + "'"));
}
}
#JsonDeserialize(converter = ColorParser.class)
Color color;
}
Problem is releated to com.fasterxml.jackson.databind.util.EnumResolver. it uses HashMap to hold enum values and HashMap doesn't support case insensitive keys.
in answers above, all chars should be uppercase or lowercase. but I fixed all (in)sensitive problems for enums with that:
https://gist.github.com/bhdrk/02307ba8066d26fa1537
CustomDeserializers.java
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.EnumDeserializer;
import com.fasterxml.jackson.databind.module.SimpleDeserializers;
import com.fasterxml.jackson.databind.util.EnumResolver;
import java.util.HashMap;
import java.util.Map;
public class CustomDeserializers extends SimpleDeserializers {
#Override
#SuppressWarnings("unchecked")
public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
return createDeserializer((Class<Enum>) type);
}
private <T extends Enum<T>> JsonDeserializer<?> createDeserializer(Class<T> enumCls) {
T[] enumValues = enumCls.getEnumConstants();
HashMap<String, T> map = createEnumValuesMap(enumValues);
return new EnumDeserializer(new EnumCaseInsensitiveResolver<T>(enumCls, enumValues, map));
}
private <T extends Enum<T>> HashMap<String, T> createEnumValuesMap(T[] enumValues) {
HashMap<String, T> map = new HashMap<String, T>();
// from last to first, so that in case of duplicate values, first wins
for (int i = enumValues.length; --i >= 0; ) {
T e = enumValues[i];
map.put(e.toString(), e);
}
return map;
}
public static class EnumCaseInsensitiveResolver<T extends Enum<T>> extends EnumResolver<T> {
protected EnumCaseInsensitiveResolver(Class<T> enumClass, T[] enums, HashMap<String, T> map) {
super(enumClass, enums, map);
}
#Override
public T findEnum(String key) {
for (Map.Entry<String, T> entry : _enumsById.entrySet()) {
if (entry.getKey().equalsIgnoreCase(key)) { // magic line <--
return entry.getValue();
}
}
return null;
}
}
}
Usage:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class JSON {
public static void main(String[] args) {
SimpleModule enumModule = new SimpleModule();
enumModule.setDeserializers(new CustomDeserializers());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(enumModule);
}
}
I used a modification of Iago Fernández and Paul solution .
I had an enum in my requestobject which needed to be case insensitive
#POST
public Response doSomePostAction(RequestObject object){
//resource implementation
}
class RequestObject{
//other params
MyEnumType myType;
#JsonSetter
public void setMyType(String type){
myType = MyEnumType.valueOf(type.toUpperCase());
}
#JsonGetter
public String getType(){
return myType.toString();//this can change
}
}
Here's how I sometimes handle enums when I want to deserialize in a case-insensitive manner (building on the code posted in the question):
#JsonIgnore
public void setDataType(DataType dataType)
{
type = dataType;
}
#JsonProperty
public void setDataType(String dataType)
{
// Clean up/validate String however you want. I like
// org.apache.commons.lang3.StringUtils.trimToEmpty
String d = StringUtils.trimToEmpty(dataType).toUpperCase();
setDataType(DataType.valueOf(d));
}
If the enum is non-trivial and thus in its own class I usually add a static parse method to handle lowercase Strings.
Deserialize enum with jackson is simple. When you want deserialize enum based in String need a constructor, a getter and a setter to your enum.Also class that use that enum must have a setter which receive DataType as param, not String:
public class Endpoint {
public enum DataType {
JSON("json"), HTML("html");
private String type;
#JsonValue
public String getDataType(){
return type;
}
#JsonSetter
public void setDataType(String t){
type = t.toLowerCase();
}
}
public String url;
public DataType type;
public Endpoint() {
}
public void setType(DataType dataType){
type = dataType;
}
}
When you have your json, you can deserialize to Endpoint class using ObjectMapper of Jackson:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
Endpoint endpoint = mapper.readValue("{\"url\":\"foo\",\"type\":\"json\"}", Endpoint.class);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
Deserializing object using FastJson with putDeserializer leads to StackOverflowError. What am I doing wrong?
test.json
{
"openapi": "3.0.1",
"info": {
"title": "Swagger Petstore",
"version": "1.0.0",
"description": "This is a sample server Petstore server"
}
}
Spec.java
#Data
public class Spec {
private String openapi;
private Info info;
}
Info.java
#Data
public class Info {
private String title;
private String version;
private String description;
}
InfoDeserializer.java
public class InfoDeserializer implements ObjectDeserializer {
#Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object o) {
Info info = parser.parseObject(Info.class);
return (T) info;
}
#Override
public int getFastMatchToken() {
return 0;
}
}
FastJsonTest.java
public class FastJsonTest {
#Test
public void test() throws IOException {
// Read json file
File file = new ClassPathResource("test.json").getFile();
String json = new String(Files.readAllBytes(file.toPath()));
// Parse json
ParserConfig config = new ParserConfig();
config.putDeserializer(Info.class, new InfoDeserializer());
// This line leads to StackOverflow
Spec spec = JSON.parseObject(json, Spec.class, config);
// Assertion
assertNotNull(spec);
}
}
However it works if I move deserializer from ParserConfig into JSONField annotation, but with this approach I am not able to pass custom parameters into deserializer.
Spec.java
#Data
public class Spec {
private String openapi;
#JSONField(deserializeUsing = InfoDeserializer.class)
private Info info;
}
FastJsonTest.java
public class FastJsonTest {
#Test
public void test() throws IOException {
// Read json file
File file = new ClassPathResource("test.json").getFile();
String json = new String(Files.readAllBytes(file.toPath()));
// Parse json
Spec spec = JSON.parseObject(json, Spec.class);
// Assertion
assertNotNull(spec);
}
}
After debugging have realized it is supposed to be used this way
public class InfoDeserializer implements ObjectDeserializer {
#Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object o) {
// Parse to JSONObject first, then parse to your object
Info info = JSON.parseObject(parser.parseObject().toJSONString(), type);
return (T) info;
}
#Override
public int getFastMatchToken() {
return 0;
}
}
I am using https://mongodb.github.io/mongo-java-driver-reactivestreams/1.11/. It seems to be using https://mongodb.github.io/mongo-java-driver/3.10/. I have a bunch of other registered classes that are working fine. I am using the suggestions at https://mongodb.github.io/mongo-java-driver/3.5/bson/pojos/ (and Save List of interface objects using mongo driver for java) for dealing with fields that have interfaces. However, I get the below error. For other classes for which I get this error, I can simply add an empty constructor to the class, but I cannot do so for an interface. Any help would be appreciated.
Caused by: org.bson.codecs.configuration.CodecConfigurationException: Failed to decode 'SearchCriteria'. Decoding 'filters' errored with: Cannot find a public constructor for 'FilterInterface'.
at org.bson.codecs.pojo.PojoCodecImpl.decodePropertyModel(PojoCodecImpl.java:222)
at org.bson.codecs.pojo.PojoCodecImpl.decodeProperties(PojoCodecImpl.java:197)
at org.bson.codecs.pojo.PojoCodecImpl.decode(PojoCodecImpl.java:121)
at org.bson.codecs.pojo.PojoCodecImpl.decode(PojoCodecImpl.java:125)
at org.bson.codecs.pojo.LazyPojoCodec.decode(LazyPojoCodec.java:57)
at org.bson.codecs.DecoderContext.decodeWithChildContext(DecoderContext.java:93)
at org.bson.codecs.pojo.PojoCodecImpl.decodePropertyModel(PojoCodecImpl.java:213)
... 36 common frames omitted
Caused by: org.bson.codecs.configuration.CodecConfigurationException: Cannot find a public constructor for 'FilterInterface'.
at org.bson.codecs.pojo.CreatorExecutable.checkHasAnExecutable(CreatorExecutable.java:140)
at org.bson.codecs.pojo.CreatorExecutable.getInstance(CreatorExecutable.java:107)
at org.bson.codecs.pojo.InstanceCreatorImpl.<init>(InstanceCreatorImpl.java:40)
at org.bson.codecs.pojo.InstanceCreatorFactoryImpl.create(InstanceCreatorFactoryImpl.java:28)
at org.bson.codecs.pojo.ClassModel.getInstanceCreator(ClassModel.java:71)
at org.bson.codecs.pojo.PojoCodecImpl.decode(PojoCodecImpl.java:120)
at org.bson.codecs.pojo.PojoCodecImpl.decode(PojoCodecImpl.java:125)
at org.bson.codecs.pojo.CollectionPropertyCodecProvider$CollectionCodec.decode(CollectionPropertyCodecProvider.java:74)
at org.bson.codecs.pojo.CollectionPropertyCodecProvider$CollectionCodec.decode(CollectionPropertyCodecProvider.java:43)
at org.bson.codecs.DecoderContext.decodeWithChildContext(DecoderContext.java:93)
at org.bson.codecs.pojo.PojoCodecImpl.decodePropertyModel(PojoCodecImpl.java:213)
... 42 common frames omitted
Below are snippets of my code:
#BsonDiscriminator
public interface FilterInterface<T> {
boolean applyOn(T value);
T getValue();
...
}
public abstract class Filter<T> implements FilterInterface<T> {
public Filter() { }
public abstract boolean applyOn(T value);
public abstract T getValue();
...
}
public class AddressFilter extends Filter<Address> {
public AddressFilter() { }
public boolean applyOn(Address value) {
return true;
}
public Address getValue() {
return new Address();
}
...
}
public class SearchCriteria {
public SearchCriteria() { }
private List<FilterInterface> filters;
}
public static void init() {
String url = <hidden>;
MongoClient mongoClient = MongoClients.create(new ConnectionString(url));
// For POJOs here
// For interface classes.
PojoCodecProvider pojoCodecProvider = PojoCodecProvider.builder()
.conventions(ImmutableList.of(CLASS_AND_PROPERTY_CONVENTION, ANNOTATION_CONVENTION))
.register(SearchCriteria.class)
.register(
ClassModel.builder(FilterInterface.class).enableDiscriminator(true).build(),
ClassModel.builder(Filter.class).enableDiscriminator(true).build(),
ClassModel.builder(AddressFilter.class).enableDiscriminator(true).build())
.automatic(true)
.build();
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(pojoCodecProvider));
String dbName = <hidden>;
mongoDb = mongoClient.getDatabase(dbName).withCodecRegistry(codecRegistry);
}
The example provided in the link works perfectly fine. Much credit goes to that user for this answer.
You have probably inserted the records when FilterInterface was a class or before using the discriminators.
Solution:
Dropping the collection and re-populating will work smoothly.
If it's production scenario, you might wanna add the field _t manually to each document.
Tip: Always use the same code for serialization and deserialization.
Explanation:
Referring to the documentation of the c-sharp driver.
The default discriminator conventions both use an element named _t to store the discriminator value in the BSON document.
If you have inserted the documents before enabling the discriminators, there would be no field _t in the document. When the driver starts decoding, it won't find and fallback to default decoder for the interface FilterInterface.
On the other hand, if you have inserted the documents when FilterInterface was a class, the value of _t will be the fully qualified name of the class. When the decoder starts to decode, it will get the ClassModel and try to create an instance of FilterInterface. Since it is now an interface, the decoder won't find the constructor.
Here is some additional info: you can change the field _t to any other name and you can specify the discriminator value by using over the classes.
#BsonDiscriminator(key = "<field_id>", value = "<value>")
Here is the modified version of the example of that answer. Please run it with discriminators disabled and then run it with discriminators enabled. You will face the same error as yours. Then clean the collection and then try again.
package org.bson.codecs.chng;
import com.google.common.collect.Lists;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.ClassModel;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.bson.conversions.Bson;
import java.util.Arrays;
import java.util.List;
public class MongoInterfaceTest {
private static MongoClient mongoClient;
static {
init();
}
public static void init() {
try {
ClassModel<User> userClassModel = ClassModel.builder(User.class).enableDiscriminator(false).build();
ClassModel<JavaUser> javaUserClassModel = ClassModel.builder(JavaUser.class).enableDiscriminator(false).build();
ClassModel<PythonUser> pythonUserClassModel = ClassModel.builder(PythonUser.class).enableDiscriminator(false).build();
ClassModel<TestUser> testUserClassModel = ClassModel.builder(TestUser.class).enableDiscriminator(false).build();
CodecRegistry pojoCodecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(
PojoCodecProvider.builder()
.register(
userClassModel,
javaUserClassModel,
pythonUserClassModel,
testUserClassModel
)
.build()
)
);
mongoClient = MongoClients.create(
MongoClientSettings.builder()
.codecRegistry(pojoCodecRegistry)
.applyConnectionString(new ConnectionString(ApplictaionConfig.MONGODB_URL))
.applyToConnectionPoolSettings(builder -> {
builder.minSize(10);
})
.build()
);
} catch (Exception e) {
System.out.println("Connection mongodb failed");
throw new RuntimeException();
}
}
public static void main(String[] args) {
MongoCollection<TestUser> collection = getMongoCollection("TestUser", TestUser.class);
JavaUser javaUser = new JavaUser<Integer>("a");
PythonUser pythonUser = new PythonUser<String>("b", "1");
TestUser testUser = new TestUser(javaUser.name, javaUser);
insertOne(collection, testUser);
testUser = new TestUser(pythonUser.name, pythonUser);
insertOne(collection, testUser);
Bson bson = Filters.and(Filters.eq("name", "a"));
TestUser testUser1 = findFirst(collection, bson);
System.out.println(testUser1);
testUser1.users.forEach(x -> System.out.println(x.dev()));
bson = Filters.and(Filters.eq("name", "b"));
testUser1 = findFirst(collection, bson);
System.out.println(testUser1);
testUser1.users.forEach(x -> System.out.println(x.dev()));
}
/**
* 获得collection对象
*/
public static <T> MongoCollection<T> getMongoCollection(String collectionName, Class<T> tClass) {
MongoDatabase mongoDatabase = mongoClient.getDatabase("kikuu");
MongoCollection<T> collection = mongoDatabase.getCollection(collectionName, tClass);
return collection;
}
public static <T> void insertOne(MongoCollection<T> collection, T document) {
insertMany(collection, Lists.newArrayList(document));
}
public static <T> void insertMany(MongoCollection<T> collection, List<T> documents) {
collection.insertMany(documents);
}
public static <T> T findFirst(MongoCollection<T> collection) {
return (T) collection.find().first();
}
public static <T> T findFirst(MongoCollection<T> collection, Bson bson) {
return (T) collection.find(bson).first();
}
public static interface User<T> {
String dev();
T foo();
}
public static class JavaUser<T> implements User<T> {
public String name;
public JavaUser() {
}
public JavaUser(String name) {
this.name = name;
}
#Override
public String dev() {
return "java";
}
#Override
public String toString() {
return "JavaUser{" +
"name='" + name + '\'' +
'}';
}
#Override
public T foo() {
return null;
}
}
public static class PythonUser<T> implements User<T> {
public String name;
public String age;
public PythonUser() {
}
public PythonUser(String name, String age) {
this.name = name;
this.age = age;
}
#Override
public String dev() {
return "python";
}
#Override
public String toString() {
return "PythonUser{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
#Override
public T foo() {
return null;
}
}
public static class TestUser {
public String name;
public List<User> users;
public TestUser() {
}
public TestUser(String name, User... users) {
this.name = name;
this.users = Arrays.asList(users);
}
#Override
public String toString() {
return "TestUser{" +
"name='" + name + '\'' +
", user=" + users +
'}';
}
}
}
I have these classes:
#XStreamAlias("person")
public class PersonConfig {
private AnimalConfig animalConfig;
}
public interface AnimalConfig {}
#XStreamAlias("dog");
public class DogConfig extend AnimalConfig {}
#XStreamAlias("cat");
public class CatConfig extend AnimalConfig {}
And I would like to be able to deserialize this xml with the classes above:
<person>
<dog/>
<person>
As well as deserialize this xml too, with the same classes:
<person>
<cat/>
<person>
So that in both cases, the PersonConfig's field animalConfig is filled. In the first XML with a DogConfig instance and in the second XML with a CatConfig instance.
Is this possible by adding some annotation to make this work?
It seems XStream does not allow you to do it easily.
Your question is similar to this one, asking for managing something like a xsd:choice with XStream.
If you don't necessarily need to use XStream, JAXB will allow you to do it easily :
#XmlRootElement(name="person")
public class PersonConfig {
private AnimalConfig animalConfig;
#XmlElementRefs({
#XmlElementRef(name="cat", type=CatConfig.class),
#XmlElementRef(name="dog", type=DogConfig.class)
})
public AnimalConfig getAnimalConfig() {
return animalConfig;
}
public void setAnimalConfig(AnimalConfig animalConfig) {
this.animalConfig = animalConfig;
}
}
After some researches, listing all available classes for your property can be avoided if you choose to use the XmlAdapter.
In Blaise Doughan link, the example uses an abstract class, not an interface.
Edit :
As Blaise Doughan said in its comment, #XmlElementRef is better suited for this purpose. Code has been updated accordingly.
You can write a converter.
public class CustomConverter implements Converter {
public void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
// TODO: Get annotation value from object 'source' with name of tag via Reflection.
// Or add a method to the AnimalConfig interface giving you tag name to put to serialization output.
}
public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
// TODO: use reflection to create animal object based on what you xml tag you have at hahd.
return context.convertAnother(context.currentObject(), SomeAnimalClazz.class);
}
public boolean canConvert(Class type) {
return type.equals(AnimalConfig.class);
}
}
There's a disadvantage: polymorphism will require you to use Java Reflection API and performance degradation.
This is quite easy. You just have to do it right and not like my previous speakers. When you process the annotations, XStream can assign those classes.
#XStreamAlias("person")
public class PersonConfig {
private AnimalConfig animalConfig;
public String toXml() {
XStream xstream = new XStream();
xstream.processAnnotations(DogConfig.class);
xstream.processAnnotations(CatConfig.class);
return xstream.toXML(this);
}
}
public interface AnimalConfig {}
#XStreamAlias("dog");
public class DogConfig implements AnimalConfig {}
#XStreamAlias("cat");
public class CatConfig implements AnimalConfig {}
It works out of the box, with out any annotations...
private static interface Test {
String getName();
Params getParams();
}
private static interface Params {
}
private static class OneParams implements Params {
private String oneValue;
public String getOneValue() {
return oneValue;
}
public void setOneValue(String oneValue) {
this.oneValue = oneValue;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("OneParams [oneValue=");
builder.append(oneValue);
builder.append("]");
return builder.toString();
}
}
private static class TwoParams implements Params {
private String twoValue;
public String getTwoValue() {
return twoValue;
}
public void setTwoValue(String twoValue) {
this.twoValue = twoValue;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("TwoParams [twoValue=");
builder.append(twoValue);
builder.append("]");
return builder.toString();
}
}
private static class OneTest implements Test {
private String name;
private Params params;
#Override
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public Params getParams() {
return params;
}
public void setParams(Params params) {
this.params = params;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("OneTest [name=");
builder.append(name);
builder.append(", params=");
builder.append(params);
builder.append("]");
return builder.toString();
}
}
---- now deserialize like this...
System.out
.println(ser
.deserialize("<XStreamTest_-OneTest><name>OneTest</name><params class=\"XStreamTest$OneParams\"><oneValue>1</oneValue></params></XStreamTest_-OneTest>"));
System.out
.println(ser
.deserialize("<XStreamTest_-OneTest><name>TwoTest</name><params class=\"XStreamTest$TwoParams\"><twoValue>2</twoValue></params></XStreamTest_-OneTest>"));
I want to achieve the following xml using simple xml framework (http://simple.sourceforge.net/):
<events>
<course-added date="01/01/2010">
...
</course-added>
<course-removed date="01/02/2010">
....
</course-removed>
<student-enrolled date="01/02/2010">
...
</student-enrolled>
</events>
I have the following (but it doesn't achieve the desired xml):
#Root(name="events")
class XMLEvents {
#ElementList(inline=true)
ArrayList<XMLEvent> events = Lists.newArrayList();
...
}
abstract class XMLEvent {
#Attribute(name="date")
String dateOfEventFormatted;
...
}
And different type of XMLNodes that have different information (but are all different types of events)
#Root(name="course-added")
class XMLCourseAdded extends XMLEvent{
#Element(name="course")
XMLCourseLongFormat course;
....
}
#Root(name="course-removed")
class XMLCourseRemoved extends XMLEvent {
#Element(name="course-id")
String courseId;
...
}
How should I do the mapping or what should I change in order to be able to achieve de desired xml?
Thanks!
A very clean way to solve the problem is to create your own converter such as:
public class XMLEventsConverter implements Converter<XMLEvents> {
private Serializer serializer;
XMLEventsConverter(Serializer serializer){
this.serializer = serializer;
}
#Override
public XMLEvents read(InputNode arg0) throws Exception {
return null;
}
#Override
public void write(OutputNode node, XMLEvents xmlEvents) throws Exception {
for (XMLEvent event : xmlEvents.events) {
serializer.write(event, node);
}
}
}
And then use a RegistryStrategy and bind the class XMLEvents with the previous converter:
private final Registry registry = new Registry();
private final Serializer serializer = new Persister(new RegistryStrategy(registry));
....
registry.bind(XMLEvents.class, new XMLEventsConverter(serializer));
In this way the xml obtained is the one desired. Note that the read method on the XMLEventsConverter just return null, in case you need to rebuild the object from the xml file you should properly implement it.
Regards!
Looks like you can do this now using the #ElementListUnion functionality
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.ElementListUnion;
import org.simpleframework.xml.core.Persister;
import java.io.StringReader;
import java.util.List;
/**
* Created by dan on 3/09/16.
*/
public class XMLEventsTest {
public static final String XML_EVENTS = "<?xml version=\"1.0\" " +
"encoding=\"ISO-8859-1\"?><events>" +
"<course-added date=\"2016/10/1\"/>" +
"<course-removed date=\"2016/10/2\"/>" +
"</events>";
public static void main(String args[]) throws Exception {
Persister persister = new Persister();
XMLEvents events = persister.read(XMLEvents.class, new StringReader(XML_EVENTS));
for (XMLEvent e : events.events) {
System.out.println("found event: " + e);
}
}
public static class XMLEvents {
#ElementListUnion({
#ElementList(entry = "course-added", type = XMLCourseAdded.class, inline = true),
#ElementList(entry = "course-removed", type = XMLCourseRemoved.class, inline = true),
})
private List<XMLEvent> events;
}
public static class XMLEvent {
#Attribute(name = "date")
private String dateOfEventFormatted;
public String getDateOfEventFormatted() {
return dateOfEventFormatted;
}
#Override
public String toString() {
return getClass().getSimpleName() + "[" + dateOfEventFormatted + "]";
}
}
public static class XMLCourseAdded extends XMLEvent {
}
public static class XMLCourseRemoved extends XMLEvent {
}
}
This will print out:
found event: XMLCourseAdded[2016/10/1]
found event: XMLCourseRemoved[2016/10/2]