I have the following XML structure:
<key>
<element>
someValue
</element>
<!-- lots of other elements which should be deserialized into the class -->
<other>
someOtherValue
</other>
</key>
And i use Simple to deserialize it to the following Java class:
#Root(name = "key", strict = false)
public class Key {
#Element(name = "element")
private String element;
// lots of more fields should be deserialized from xml
}
Note that the class does not have a field for the other element. I do not need the value of it in the class, but in an other place. How can i intercept the parsing and extract the value of this other element?
You can do it a number of ways, best is to use a Converter or a Strategy though. Converter is the easiest of the two.
I think the Strategy approach doesn't work, because they use the annotated class as the XML schema, and what is not present in the schema that will not be processed (visitors can't visit).
Converters can be used as follows:
#Root(name = "key", strict = false)
#Convert(KeyConverter.class)
public class Key {
private String element;
public Key(String elementValue) {
element = elementValue;
}
}
The converter stores the value during conversion:
public class KeyConverter implements Converter<Key> {
private String otherValue;
#Override
public Key read(InputNode node) throws Exception {
String elementValue = node.getNext("element").getValue().trim();
otherValue = node.getNext("other").getValue().trim();
return new Key(elementValue);
}
#Override
public void write(OutputNode arg0, Key arg1) throws Exception {
throw new UnsupportedOperationException();
}
/**
* #return the otherValue
*/
public String getOtherValue() {
return otherValue;
}
}
Putting together:
Registry registry = new Registry();
KeyConverter keyConverter = new KeyConverter();
registry.bind(Key.class, keyConverter);
Persister serializer = new Persister(new RegistryStrategy(registry));
Key key = serializer.read(Key.class, this.getClass().getResourceAsStream("key.xml"));
// Returns the value "acquired" during the last conversion
System.out.println(keyConverter.getOtherValue());
This is not too elegant, but might be suitable for your need.
I could not make a solution with a Stragegy or Converter as ng and Katona suggested. However, i made a workaround, which works, but not too nice.
/* package */ class SerializedKey extends Key {
#Element(name = "other", required = false)
private int mOtherValue;
public int getOtherValue() {
return mOtherValue;
}
}
...
Serializer serializer = new Persister();
SerializedKey key = serializer.read(SerializedKey.class, mInputStream);
int otherValue = key.getOtherValue();
Outside of the serialization package, i use Key as static type, so i simply forget that another field is in that object. When i persist my data, i also persist as a Key, so the mOtherValue is not connected to the class anymore. As you can see SerializedKey class is package-private, so i do not expose this helper class to any other component of my application.
Related
I have 2 implementations of Value interface, RangeValue, FileValue.
RangeValue looks like below:
public class RangeValue implements Value {
private int min;
private int max;
public RangeValue(int min, int max) {
this.min = min;
this.max = max;
}
public int getMin() {
return min;
}
public int getMax() {
return max;
}
}
FileValue looks like below:
public class FileValue implements Value {
private String contentType;
private String value;
public FileValue(String contentType, String value) {
this.contentType = contentType;
this.value = value;
}
public String getContentType() {
return contentType;
}
public String getValue() {
return value;
}
}
the json for RangeValue looks like :
{
"min": 200,
"max": 300
}
The json for FileValue looks:
{
"contentType": "application/octet-stream",
"value": "fileValue"
}
Now I want the RequestType parameter for these json to be of type Value only, I can't change the JSON files i.e. the json would look like the same and user should use the same JSON in request body as stated above.
I solved this by using #JsonTypeInfo & #JsonSubTypes by adding extra attributes to the above JSON i.e. type but the spec doesn't allow me to add that.
How can the appropriate concrete class could be instantiated based on the JSON above without altering?
Option 1: custom deserializer. Algorithm can be as follows:
Parse to JsonNode.
Use the properties in the node to find the correct class to deserialize into.
Convert the node to instance of the actual class.
Simplified example:
public class ValueDeserializer extends StdDeserializer<Value> {
public ValueDeserializer() {
super(Value.class);
}
#Override
public Value deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode root = parser.readValueAsTree();
if (root instanceof ObjectNode objectNode) {
JsonNode valueNode = objectNode.get("somePropertyName");
Class<? extends Value> clazz = valueNode == null ? RangeValue.class : FileValue.class;
return context.readTreeAsValue(objectNode, clazz);
}
throw new JsonParseException(parser, "not an object");
//handling the case, when json is json array
//or something else which can't be deserialized into object
}
}
Register the deserializer with JsonDeserialize on the interface:
#JsonDeserialize(using = ValueDeserializer.class)
Put the same annotation on RangeValue and FileValue, without specifying a deserializer, otherwise you will get StackOverflowError.
Option 2: use JsonTypeInfo.Id.DEDUCTION
#JsonSubTypes({
#JsonSubTypes.Type(FileValue.class),
#JsonSubTypes.Type(RangeValue.class)
})
#JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
public interface Value {
}
Jackson will deduce the correct class using the property names. Keep in mind exception will be thrown if it fails deduction.
Means that no serialized typing-property is used. Types are deduced based on the fields available. Deduction is limited to the names of fields (not their values or, consequently, any nested descendants). Exceptions will be thrown if not enough unique information is present to select a single subtype.
If deduction is being used annotation properties visible, property and include are ignored.
I have the following data model with custom attributes:
class Foo {
private Long id;
private Set<AdditionalAttribute> attributes;
}
class AdditionalAttribute {
private Key key;
private String value;
}
class Key {
private String name;
private Class<?> type;
}
My model produces this json:
{"id":123, "attributes": [{"key1":12345}, {"key2":"value2"}]}
My expected json is:
{"id":123, "key1":12345, "key2":"value2"}
How can I achieve a such serialization / deserialization using graphql spqr?
FYI, currently I can do it in REST API with jackson (BeanSerializerModifier for serialization and #JsonAnySetter for deserialization) as follow:
// Serialization using BeanSerializerModifier
class FooModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(
SerializationConfig config, BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
for (int i = 0; i < beanProperties.size(); i++) {
BeanPropertyWriter writer = beanProperties.get(i);
if (Foo.class.isAssignableFrom(beanDesc.getBeanClass()) && "attributes".equals(writer.getName())) {
beanProperties.set(i, new FooAttributesWriter(writer));
}
}
return beanProperties;
}
}
class FooAttributesWriter extends BeanPropertyWriter {
public HasAttributesWriter(BeanPropertyWriter w) {
super(w);
}
#Override
public void serializeAsField(Object bean, JsonGenerator gen,
SerializerProvider prov) throws Exception {
if(Foo.class.isAssignableFrom(bean.getClass())) {
Set<AdditionalAttribute> set = ((Foo) bean).getAttributes();
for (AdditionalAttribute a : set) {
gen.writeStringField(a.getKey().getName(), a.getValue());
}
}
}
}
// Deserilization using #JsonAnySetter
class Foo {
private Long id;
private Set<AdditionalAttribute> attributes;
// Deserialization of custom properties
#JsonAnySetter
public void set(String name, Object value) {
attributes.add(new AdditionalAttribute(buildKey(name,value), value));
}
}
The problem here is not JSON (de)serialization. With GraphQL, the shape of all your inputs and outputs is defined by the schema, and the schema can not normally have dynamic parts (object types where the fields are unknown ahead of time). Because your Set<AdditionalAttribute> can contain anything at all at runtime, it means your Foo type would have to have unknown fields. This is highly antithetical to how GraphQL is designed.
The only way to achieve a dynamic structure is to have an object scalar which effectively is a JSON blob that can not be validated, or sub-selected from. You could turn Foo into such a scalar by adding #GraphQLScalar to it. Then all input would be valid, {"id":123, "key1":12345 "key2":"value2"} but also {"whatever": "something"}. And it would be your logic's job to ensure correctness. Additionally, if you ever return Foo, the client would not be able to sub-select from it. E.g. {foo} would be possible but {foo { id }} would not, because the schema would no longer know if the id field is present.
To recap, you options are:
Leaving it as it is (the dynamic stuff is a list nested under attributes)
Turning Set<AdditionalAttribute> into a type (a new class or EnumMap) with known structure with all the possible keys as fields. This is only possible if the keys aren't totally dynamic
Making the whole enclosing object an object scalar by using #GraphQLScalar
Thanks a lot for your time and the proposed options.
Currently, we have found another way (maybe option 4 :) ) to generate a "similar" json to the expected output (We lost the type information in the generated output, but we have another logic that helps us to retrieve the type).
Here an example :
class Foo {
private Long id;
private Set<AdditionalAttribute> attributes;
#GraphQLQuery
public String get(#GraphQLArgument(name = "key") String key) {
for (AdditionalAttribute a : attributes) {
if (a.getConfigurationKey().getKey().equalsIgnoreCase(key)) {
return a.getAttributeValue();
}
}
return null;
}
and we can sub-select Foo as follow:
foo {
id
key1: get(key: "key1")
key2: get(key: "key2")
}
And this return
{"id":123, "key1":"12345", "key2":"value2"}
I am trying to unmarshall an ArrayList of a generic class called Key.
the Key has setValue() method which recieves a generic parameter.
Key class
#XMLRootElement(name = "Key")
public class Key<T>{
#XMLElement(name = "Key")
public setKey(T value){
this.value = value
}
}
The specific ArrayList
#XMLElementWrapper(name = "Keys")
#XMLElement(name = "Key")
public setKeys(ArrayList<Key> keys){
this.keys = keys;
}
This part of the XML file
<Keys>
<Key>2</Key>
</Keys>
Running the code would create the ArrayList and WILL have a single Key object in it.
But the Key would be Null.
(Ive tried debugging and could notice that it does not call the setKey() setter of the class)
Anything to do with the fact it's generic?
Thanks in advance.
EDIT
In the past day ive debugged this alot, i can say now that the problem is with the fact that after instantiating the ArrayList, while creating each Key per Key Tag in the XML, the unmarshaller uses the Key's empty constructor and just NEVER calls the setter of it, therefore i have an ArrayList containing Keys which their 'value' data member is null.
Can anyone please explain what am I doing wrong? Why does the setter not getting called?
Thank you.
You are probably out of luck. How is the unmarshaller supposed to know that 2 is an integer and not a double or a long or a timestamp or some other class with a custom adapter that can parse 2 into itself.
The annotations you want are basically below (minus the #XmlJavaTypeAdapter which I will explain in a moment) but if you try and run that code without the adapter you will get a NullPointerException because JAXB cannot handle the #XmlValue annotation on an Object (which is how it treats T). The reason JAXB cannot handle it is because it has no way of knowing what the object is.
Now, if you have your own custom rules for determining the type of T (e.g. when coming from XML T is always an Integer or T is an Integer if it doesn't contain a '.' and a Double otherwise) then you can implement your own logic using an adapter which is what I've demonstrated below (I used the second rule).
#XmlRootElement(name="root")
public class SO {
private List<Key<?>> keys;
#XmlElementWrapper(name="Keys")
#XmlElement(name="Key")
public void setKeys(List<Key<?>> keys) {
this.keys = keys;
}
public List<Key<?>> getKeys() {
return keys;
}
#XmlType
public static class Key<T> {
private T val;
#XmlValue
#XmlJavaTypeAdapter(ToStringAdapter.class)
public void setKey(T val) {
this.val = val;
}
public String toString() {
return "Key(" + val + ")";
}
}
public static class ToStringAdapter extends XmlAdapter<String, Object> {
#Override
public Object unmarshal(String v) throws Exception {
if(v.contains(".")) {
return Double.parseDouble(v);
} else {
return Integer.parseInt(v);
}
}
#Override
public String marshal(Object v) throws Exception {
return v.toString(); //Will never be called anyway so you could also throw an exception here
}
}
private static final String XML_INT = "<root><Keys><Key>2</Key></Keys></root>";
private static final String XML_DOUBLE = "<root><Keys><Key>2.7</Key></Keys></root>";
public static void main(String [] args) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(Key.class, SO.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
SO so = (SO) unmarshaller.unmarshal(new StringReader(XML_INT));
System.out.print(so.keys);
System.out.println(" " + so.keys.get(0).val.getClass().getSimpleName());
so = (SO) unmarshaller.unmarshal(new StringReader(XML_DOUBLE));
System.out.print(so.keys);
System.out.println(" " + so.keys.get(0).val.getClass().getSimpleName());
}
}
I am new to the Simple framework for XML (Java) and ran into a problem serializing specific class constructs.
I have two classes:
#Root(name="static")
class StaticData {
#Attribute
private String id;
#Attribute
private String value;
...
}
and
#Root(name="listdata")
class ListData {
// Problem field
#Attribute
private StaticData ref;
#Element
private String name;
}
And receive
"TransformException: Transform of class StaticData not supported".
I want the ref-field in ListData not to expand into the static data XML structure (then #Element would be fine), but to get a reference.
<listdata ref="foo">
<name>bla bla</name>
</listdata>
where "foo" is a valid value for "id" in some StaticData object already loaded in my application.
In JAXB I would use the XmlJavaTypeAdapter annotation
#XmlAttribute(name="id")
#XmlJavaTypeAdapter(MyStaticDataAdapter.class)
but I cannot seem to find a working equivalent in Simple.
In doubt you can use a Converter to implement such a behaviour.
Here's an example:
#Root(name = "listdata")
#Convert(ListData.ListDataConverter.class)
class ListData
{
#Attribute
private StaticData ref;
#Element
private String name;
// ...
// Converter implementation
static class ListDataConverter implements Converter<ListData>
{
#Override
public ListData read(InputNode node) throws Exception
{
/*
* In case you also want to read, implement this too ...
*/
throw new UnsupportedOperationException("Not supported yet.");
}
#Override
public void write(OutputNode node, ListData value) throws Exception
{
node.setAttribute("ref", value.ref.getId());
node.getChild("name").setValue(value.name);
}
}
}
Usage:
Serializer ser = new Persister(new AnnotationStrategy());
/* ^----- important! -----^ */
ListData ld = ...
ser.write(ld, System.out); // Serialize to std out
Output
With these ListData values ...
name = abcdefg
ref = ...
id = 123
value = this is a value
you'll get ...
<listdata ref="123">
<name>def</name>
</listdata>
I have two classes, Package and ModelRefObj. Package contains two sets of ModelRefObj.
I'm using Simple framework to parse their instances from XML, so I've created some JUnit tests. I'm able to parse ModelRefObj XML, but I'm getting the following exception when trying to parse a Package:
org.simpleframework.xml.core.ValueRequiredException: Empty value for #org.simpleframework.xml.Text(empty=, data=false, required=true) on field 'value' private java.lang.String cz.semanta.coc.domain.cognos.ModelRefObj.value in class cz.semanta.coc.domain.cognos.ModelRefObj at line 1
at org.simpleframework.xml.core.Composite.readInstance(Composite.java:580)
at org.simpleframework.xml.core.Composite.readText(Composite.java:467)
at org.simpleframework.xml.core.Composite.access$200(Composite.java:59)
at org.simpleframework.xml.core.Composite$Builder.read(Composite.java:1381)
...
Here is the XML I'm trying to parse:
<package>
<name>GO Sales (nalysis)</name>
<visible>
<refObj>[go_sales]</refObj>
<refObj>[Filters and calculations].[Returns]</refObj>
</visible>
<hidden>
<refObj>[gosales].[BRANCH].[BRANCH_CODE]</refObj>
<refObj>[gosales].[BRANCH].[ADDRESS1]</refObj>
<refObj>[gosales].[BRANCH].[CITY]</refObj>
</hidden>
</package>
Here are my annotated classes:
#Root(name = "package")
public class Package {
#Element
private String name;
#ElementList(name = "visible", entry = "refObj", type = ModelRefObj.class)
private Set<ModelRefObj> visibleRefObjs;
#ElementList(name = "hidden", entry = "refObj", type = ModelRefObj.class)
private Set<ModelRefObj> hiddenRefObjs;
Package() { }
...
}
#Root(name = "refObj")
public class ModelRefObj {
#Text
private String value;
ModelRefObj() { }
public ModelRefObj(String value) {
this.value = value;
}
...
}
I have implemented the classes you have and used the example xml you provided.
I created a main function to test
public static void main(String args[]) throws Exception {
Serializer serializer = new Persister(new Format("<?xml version=\"1.0\" encoding= \"UTF-8\" ?>"));
File source = new File("sample.xml");
Package p = serializer.read(Package.class, source);
System.out.println(p.name);
}
The output is
GO Sales (nalysis)
Inspecting the object p in debug mode shows it has the two Sets with two and three elements.
Your code works fine for me.