I am working with xstream to read some xml in the following format --
<Objects>
<Object Type="System.Management.Automation.Internal.Host.InternalHost">
<Property Name="Name" Type="System.String">ConsoleHost</Property>
<Property Name="Version" Type="System.Version">2.0</Property>
<Property Name="InstanceId" Type="System.Guid">7e2156</Property>
</Object>
</Objects>
Basically under Objects tag there can be n number of Object Type and each Object Type can have n number of Property tags. So I have modelled by Java classes and the code to read it as follows --
class ParentResponseObject {
List <ResponseObject>responseObjects = new ArrayList<ResponseObject>();
}
#XStreamAlias("Object")
#XStreamConverter(value = ToAttributedValueConverter.class, strings = { "Value" })
class ResponseObject {
String Type;
String Value;
List <Properties> properties = new ArrayList<Properties>();
}
#XStreamAlias("Property")
#XStreamConverter(value = ToAttributedValueConverter.class, strings = { "Value" })
class Properties {
String Name;
String Type;
String Value;
}
public class MyAgainTest {
public static void main (String[] args) throws Exception {
String k1 = //collect the xml as string
XStream s = new XStream(new DomDriver());
s.alias("Objects", ParentResponseObject.class);
s.alias("Object", ResponseObject.class);
s.alias("Property", Properties.class);
s.useAttributeFor(ResponseObject.class, "Type");
s.addImplicitCollection(ParentResponseObject.class, "responseObjects");
s.addImplicitCollection(ResponseObject.class, "properties");
s.useAttributeFor(Properties.class, "Name");
s.useAttributeFor(Properties.class, "Type");
s.processAnnotations(ParentResponseObject.class);
ParentResponseObject gh =(ParentResponseObject)s.fromXML(k1);
System.out.println(gh.toString());
}
}
Using this code, I am able to populate the responseObjects List in the ParentResponseObject class. However, the properties list in the ResponseObject is always null, even though I am using the same technique in both the cases. Can anyone please help on getting this solved. Help on this is highly appreciated.
Your XML format does not match your Java object model. According to the XML, <Property> is a child of <Objects>, but according to your code, the Properties list is part of the ResponseObject. You need to fix this mismatch.
Also, it seems that you are using a mix of annotations and code. Either use only annotations (recommended) or do it all in code. Otherwise, your code becomes confusing and unreadable.
Update:
I see you have fixed your XML. The problem is that you have a Value field in your ResponseObject, but there is no value in the xml element, so remove it.
The following code should work:
#XStreamAlias("Objects")
public class ParentResponseObject {
#XStreamImplicit
List<ResponseObject> responseObjects = new ArrayList<ResponseObject>();
}
#XStreamAlias("Object")
public class ResponseObject {
#XStreamAsAttribute
String Type;
#XStreamImplicit
List<Properties> properties = new ArrayList<Properties>();
}
#XStreamAlias("Property")
#XStreamConverter(value = ToAttributedValueConverter.class, strings = { "Value" })
public class Properties {
String Name;
String Type;
String Value;
}
Main method:
XStream s = new XStream(new DomDriver());
s.processAnnotations(ParentResponseObject.class);
ParentResponseObject gh = (ParentResponseObject) s.fromXML(xml);
for (ResponseObject o : gh.responseObjects) {
System.out.println(o.Type);
for (Properties p : o.properties) {
System.out.println(p.Name + ":" + p.Type + ":" + p.Value);
}
}
Related
My service can receive several different jsons, such as:
{
"event":"conversation_started",
"context":"context information",
"user":{
"id":"01234567890A=",
"name":"John McClane",
"avatar":"http://avatar.example.com",
"country":"UK",
"language":"en",
"api_version":1
},
"subscribed":false
}
or
{
"event":"message",
"message":{
"type":"text",
"text":"a message to the service",
"location":{
"lat":12.34,
"lon":12.34
}
}
}
or several else jsons. The only field that is the same for all jsons is "event". All other fields can be different (depends on "event" value).
So the question is: how to convert those jsons to java objects (without making messy code)? The only way I know is to manually check "event" value (like json.startsWith("{\n\"event\":\"message\"") but I'm sure that there is any simple decision for doing this.
There are three ways I've done this. The first is to do what you're suggesting - parse the JSON, check the type, and create the object. Be very careful with using a String parser as you may or may not have things like new lines. Instead, do something like:
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(eventString);
String eventType = jsonNode.get("event").asText();
if( eventType.equalsIgnoreCase("conversation_started")) {
// create ConversationStarted object using something like:
ConversationStarted conversationStarted = objectMapper.readValue( eventString, ConversationStarted.class );
}
This, of course, requires all classes to have a concrete POJO to allow for deserialization.
Another way is to do what many other programming languages do and have a key/value map. There are a few ways to do this. One is with the Jackson libraries:
Map<String, Object> map = objectMapper.readValue(eventString, new TypeReference<Map<String,Object>>(){});
Map<String, Object> user = (Map<String, Object>) map.get("user");
System.out.println( "conversation started - avatar is " + user.get("avatar"));
That way you can pass around the Map and extract as needed. Note that you still need to understand the structure of the JSON but you don't need to have a POJO for it.
Lastly is a variation on the second solution. Using JSONPath you can pull out what you need directly. Again you will want to first check out which type of event you have. Something like:
if( JsonPath.read(eventString, "$.event").equals("conversation_started") ) {
String avatar = JsonPath.read(eventString, "$.user.avatar");
System.out.println("conversation started - avatar is " + avatar);
}
The last two methods require you to pull out values one at a time as shown. The first solution gives you a full object to work with. It is your call as to what works best in your environment.
UPD: If you don't want to convert JSON String to JAVA Object via declaring a POJO, you can parse it to JSONObject(com.alibaba.fastjson.JSONObject)
public class Event {
public static void main(String[] args) {
String jsonA = "{\"event\":\"conversation_started\",\"context\":\"context information\",\"user\":{\"id\":\"01234567890A=\",\"name\":\"John McClane\",\"avatar\":\"http://avatar.example.com\",\"country\":\"UK\",\"language\":\"en\",\"api_version\":1},\"subscribed\":false}";
String jsonB = "{\"event\":\"message\",\"message\":{\"type\":\"text\",\"text\":\"a message to the service\",\"location\":{\"lat\":12.34,\"lon\":12.34}}}";
JSONObject jsonObject = JSONObject.parseObject(jsonA);
String event = jsonObject.getString("event");
if (event.equals("message")) {
//do what you want to do
System.out.println("message event......");
} else if ("conversation_started".equals(event)) {
System.out.println("context information event......");
}
}
}
Declaring a class of Event as below, and then convert JSON String to a Event JAVA object.
#Data
public class Event {
private String event;
private String context;
private User user;
private boolean subscribed;
private Message message;
#Data
public static class User {
private String id;
private String name;
private String avatar;
private String country;
private String language;
private int api_version;
}
#Data
public static class Message {
private String type;
private String text;
private Location location;
#Data
public static class Location {
private double lat;
private double lon;
}
}
public static void main(String[] args) {
String jsonA = "{\"event\":\"conversation_started\",\"context\":\"context information\",\"user\":{\"id\":\"01234567890A=\",\"name\":\"John McClane\",\"avatar\":\"http://avatar.example.com\",\"country\":\"UK\",\"language\":\"en\",\"api_version\":1},\"subscribed\":false}";
String jsonB = "{\"event\":\"message\",\"message\":{\"type\":\"text\",\"text\":\"a message to the service\",\"location\":{\"lat\":12.34,\"lon\":12.34}}}";
ObjectMapper objectMapper = new ObjectMapper();
try {
Event eventA = objectMapper.readValue(jsonA, new TypeReference<Event>() {
});
System.out.println(objectMapper.writeValueAsString(eventA));
Event eventB = objectMapper.readValue(jsonB, new TypeReference<Event>() {
});
System.out.println(objectMapper.writeValueAsString(eventB));
} catch (IOException e) {
e.printStackTrace();
}
}
}
Use a JSON object. This is dynamic and can load any json. Then you can reference the event field consistently
Example 1
//import java.util.ArrayList;
//import org.bson.Document;
Document root = Document.parse("{ \"event\" : \"conversation_started\", \"context\" : \"context information\", \"user\" : { \"id\" : \"01234567890A=\", \"name\" : \"John McClane\", \"avatar\" : \"http://avatar.example.com\", \"country\" : \"UK\", \"language\" : \"en\", \"api_version\" : 1 }, \"subscribed\" : false }");
System.out.println(((String)root.get("event")));
Example 2
//import java.util.ArrayList;
//import org.bson.Document;
Document root = Document.parse("{ \"event\" : \"message\", \"message\" : { \"type\" : \"text\", \"text\" : \"a message to the service\", \"location\" : { \"lat\" : 12.34, \"lon\" : 12.34 } } }");
System.out.println(((String)root.get("event")));
I've got a nested object hierarchy which looks like this:
Profile: contains a List<Category>
Category contains a List<Script>. It is exposed via a JavaFX SimpleListProperty, so that it can be bound to via JavaFX's data binding.
Script contains nothing but simple values.
I'm just using JAXB to marshall and unmarshall POJOs. There are no databases or XML schemas involved.
Marshalling a Profile value works fine, and generates valid XML. However, unmarshalling the same XML file later, results in each Category containing an empty List<Script>. This appears to be due to the fact that Category stores the List<Script> by using a JavaFX bindable property.
Is there a way to make JAXB properly deserialize into a SimpleListProperty that contains a custom object?
Here's a minimal sample that demonstrates the same issue.
public class Main
{
public static void main(String[] args) throws Exception
{
Script script1 = new Script();
script1.name = "Script 1";
script1.otherData = "Script 1's data";
Script script2 = new Script();
script2.name = "Script 2";
script2.otherData = "Script 2's data";
ArrayList<Script> scriptList = new ArrayList<Script>();
scriptList.add(script1);
scriptList.add(script2);
Category category1 = new Category();
category1.name = "Category 1";
category1.setCategoryScripts(scriptList);
Category category2 = new Category();
category2.name = "Category 2";
category2.setCategoryScripts(scriptList);
Profile profile = new Profile();
profile.name = "Profile 1";
profile.categories.add(category1);
profile.categories.add(category2);
JAXBContext context = JAXBContext.newInstance(Profile.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
StringWriter xml = new StringWriter();
m.marshal(profile, xml);
System.out.println(xml.toString());
Profile deserializedProfile = (Profile)context
.createUnmarshaller()
.unmarshal(new StringReader(xml.toString()));
System.out.println("Profile: " + deserializedProfile.name);
for(Category cat : deserializedProfile.categories)
{
System.out.println("Category: " + cat.name);
System.out.println("Scripts:");
for(Script s : cat.getCategoryScripts())
{
System.out.printf("\nName: %s, Data: %s", s.name, s.otherData);
}
}
}
}
#XmlRootElement
class Profile
{
#XmlElement
String name;
#XmlElementWrapper
#XmlElement
ArrayList<Category> categories = new ArrayList<Category>();
}
#XmlRootElement
class Category
{
#XmlElement
String name;
ListProperty<Script> categoryScripts = new SimpleListProperty<Script>();
#XmlElementWrapper
#XmlElement
public final List<Script> getCategoryScripts() { return categoryScripts.get(); }
public final void setCategoryScripts(List<Script> value) { categoryScripts.set(FXCollections.observableArrayList(value)); }
public ListProperty<Script> categoryScriptProperty() { return categoryScripts; }
}
#XmlRootElement
class Script
{
#XmlElement
String name;
#XmlElement
String otherData;
}
One of the principles of StackOverflow is Minimal, Complete, and Verifiable questions. See https://stackoverflow.com/help/mcve
When creating such test code, I did this:
public class Test {
public static void main(String[] args) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(Categories.class);
Categories categoriesIn = new Categories();
categoriesIn.scripts.add(new Script("Hello"));
categoriesIn.scripts.add(new Script("World"));
StringWriter xml = new StringWriter();
jaxbContext.createMarshaller().marshal(categoriesIn, xml);
System.out.println(xml.toString());
Categories categoriesOut = (Categories)jaxbContext.createUnmarshaller().unmarshal(new StringReader(xml.toString()));
System.out.println(categoriesOut.scripts.size() + " scripts:");
for (Script script : categoriesOut.scripts)
System.out.println(" " + script.name);
}
}
#XmlRootElement
class Categories {
#XmlElementWrapper(name = "Scripts")
#XmlElement(name = "Script")
List<Script> scripts = new ArrayList<>();
}
class Script {
#XmlElement(name = "name")
String name;
Script() {}
Script(String name) { this.name = name; }
}
Running it produced:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><categories><Scripts><Script><name>Hello</name></Script><Script><name>World</name></Script></Scripts></categories>
2 scripts:
Hello
World
It seems to work fine, on Java 8 at least.
Try it out. If it doesn't work, then you might need to upgrade Java to a newer version. If it works, you might use it as a baseline to see what might be different in your code.
I believe you're running into a weirdness of JAXB. Notice that you don't have an addCategoryScript method. So how is JAXB going to add Script objects to the list?
It could create a list of it's own, then give it to you with setCategoryScripts, but how would it know what kind of list to create?
It solves that dilemma by calling getCategoryScripts to get the initial (empty) list, and then add elements to it.
But then what happens if you returned a copy of the internal list?
Ah Ha! Call setCategoryScripts when the list is complete.
This means that it'll call setCategoryScripts with the list returned by getCategoryScripts.
I has an implementation that needed some special handling, so what I did was:
public List<MyObj> getMyList() {
return this.myList;
}
public void setMyList(List<MyObj> myList) {
this.myList.clear();
for (MyObj o : myList)
this.myList.add(handle(o));
}
But oops. The call to clear actually cleared the incoming objList parameter, and I ended up with nothing.
My solution was to copy the parameter list before proceeding.
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.
I have a class like this:
public class DeserializedHeader
int typeToClassId;
Object obj
I know what type of object obj is based on the typeToClassId, which is unfortunately only known at runtime.
I want to parse obj out based on typeToClassId - what's the best approach here? Annotations seem like they're out, and something based on ObjectMapper seems right, but I'm having trouble figuring out what the best approach is likely to be.
Something along the lines of
Class clazz = lookUpClassBasedOnId(typeToClassId)
objectMapper.readValue(obj, clazz)
Obviously, this doesn't work since obj is already deserialized... but could I do this in 2 steps somehow, perhaps with convertValue?
This is really complex and painful problem. I do not know any sophisticated and elegant solution, but I can share with you my idea which I developed. I have created example program which help me to show you how you can solve your problem. At the beginning I have created two simple POJO classes:
class Product {
private String name;
// getters/setters/toString
}
and
class Entity {
private long id;
// getters/setters/toString
}
Example input JSON for those classes could look like this. For Product class:
{
"typeToClassId" : 33,
"obj" : {
"name" : "Computer"
}
}
and for Entity class:
{
"typeToClassId" : 45,
"obj" : {
"id" : 10
}
}
The main functionality which we want to use is "partial serializing/deserializing". To do this we will enable FAIL_ON_UNKNOWN_PROPERTIES feature on ObjectMapper. Now we have to create two classes which define typeToClassId and obj properties.
class HeaderType {
private int typeToClassId;
public int getTypeToClassId() {
return typeToClassId;
}
public void setTypeToClassId(int typeToClassId) {
this.typeToClassId = typeToClassId;
}
#Override
public String toString() {
return "HeaderType [typeToClassId=" + typeToClassId + "]";
}
}
class HeaderObject<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
#Override
public String toString() {
return "HeaderObject [obj=" + obj + "]";
}
}
And, finally source code which can parse JSON:
// Simple binding
Map<Integer, Class<?>> classResolverMap = new HashMap<Integer, Class<?>>();
classResolverMap.put(33, Product.class);
classResolverMap.put(45, Entity.class);
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
String json = "{...}";
// Parse type
HeaderType headerType = mapper.readValue(json, HeaderType.class);
// Retrieve class by integer value
Class<?> clazz = classResolverMap.get(headerType.getTypeToClassId());
// Create dynamic type
JavaType type = mapper.getTypeFactory().constructParametricType(HeaderObject.class, clazz);
// Parse object
HeaderObject<?> headerObject = (HeaderObject<?>) mapper.readValue(json, type);
// Get the object
Object result = headerObject.getObj();
System.out.println(result);
Helpful links:
How To Convert Java Map To / From JSON (Jackson).
java jackson parse object containing a generic type object.
I have following, problem with XStream: when I try to read the annotations I need to use following sentence:
xstream.processAnnotations(DataClass .class);
which defines explicitly the class I'm going to serialize. But in my code:
public class Tester {
/**
* #param args
*/
public static void main(String[] args) {
DataClass data = new DataClass();
data.familyName = "Pil";
data.firstName = "Paco";
data.ID = 33;
data.properties.put("one", "1");
data.properties.put("two", "2");
data.properties.put("three", "3");
String xml = getXmlString(data);
System.out.println(xml);
}
public static String getXmlString(Object data) {
String ret = "";
final StringWriter stringWriter = new StringWriter();
XStream xstream = new XStream(new StaxDriver());
xstream.processAnnotations(Object.class);
xstream.marshal(data, new PrettyPrintWriter(stringWriter));
ret = stringWriter.toString();
return ret;
}
}
where the dataClass is:
#XStreamAlias("data")
public class DataClass {
public Integer ID = 0;
public String firstName = "";
public String familyName = "";
public Map<String, String> properties = null;
public DataClass(){
properties = new HashMap<String,String>();
}
}
I would like to have something like that:
public static <T> String getXmlString(T data) {
String ret = "";
final StringWriter stringWriter = new StringWriter();
XStream xstream = new XStream(new StaxDriver());
xstream.processAnnotations(T.class);
xstream.marshal(data, new PrettyPrintWriter(stringWriter));
ret = stringWriter.toString();
return ret;
}
but it doesn't work.
Do anyone know if it is possible what I'm trying to do?
Probably you should enabled "Auto-detect Annotations" mode:
public static <T> String getXmlString(T data) {
final StringWriter stringWriter = new StringWriter();
XStream xstream = new XStream(new StaxDriver());
xstream.autodetectAnnotations(true);
xstream.marshal(data, new PrettyPrintWriter(stringWriter));
return stringWriter.toString();
}
Please, read "Auto-detect Annotations" paragraph. You can find in it all information about problems which are linked with this solution. Such as: Chicken-and-egg problem, Concurrency, Exceptions and Performance.
Result:
<data>
<ID>33</ID>
<firstName>Paco</firstName>
<familyName>Pil</familyName>
<properties>
<entry>
<string>two</string>
<string>2</string>
</entry>
<entry>
<string>one</string>
<string>1</string>
</entry>
<entry>
<string>three</string>
<string>3</string>
</entry>
</properties>
</data>
I recommend you creat an init() method that initializes your xstream object. Let your xstream object have class scope. I have never had a reason to create more than one xstream object within a project, so I usually handle it this way. You could even make it a static field. Place all of your annotation processing methods within init() and explicitly register every class you expect to serialize.
You could use
if(data != null) xstream.processAnnotations(data.getClass());
to access the direct class of the data object, but as Thorn suggests it's probably better to declare a single XStream instance that knows about all the classes you will be serializing.
I think I found one solution, it may be not the best, but as I need to really be VERY flexible, it the solution that best fit my needs:
public static <T> String getXmlString(T data, Class<?> dataClass) {
String ret = "";
final StringWriter stringWriter = new StringWriter();
XStream xstream = new XStream(new StaxDriver());
xstream.processAnnotations(dataClass);
xstream.marshal(data, new PrettyPrintWriter(stringWriter));
ret = stringWriter.toString();
return ret;
}
I will be very happy if someone find somethig better. I'm quite new to Java and probably I'm not using all the cool features of this language.
Maybe not directly related to this question but if you use Spring XStreamMarshaller, you need to turn on the detection of XStream annotations as follow:
<bean id="xStreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="autodetectAnnotations" value="true"/>
</bean>