3

The code below used to work under the JAXB implementation used by JDK 1.7, but now under JDK 1.8 it's broken. In the code below you will find the key change that seems to make it work in 1.8. The "fix" under 1.8 is not really a fix because it's bad practice to expose internal collections for direct modification by the outside world. I want to control access to the internal list through my class and I don't want to complicate things by making observable collections and listening to them. This is not acceptable.

Is there any way to get my original code to work under the JAXB of JD 1.8?

 @XmlElementWrapper(name = "Wrap")
   @XmlElement(name = "Item", required = true)
   public synchronized void setList(List<CustomObject> values) {
     list.clear();
     list.addAll(values);
   }

public synchronized List<CustomObject> getList() {
//      return new ArrayList(list); // this was the original code that worked under 1.7
      return list; //this is the only thing that works under 1.8
   }

After more analysis, the problem seems to be coming from JAXB not calling the setter method for collections anymore (it used to under JDK 1.7). Now under JDK 1.8, it calls the getter and modifies the collection directly. This poses several problems:

1-forces the user to expose an internal collection to the outside world for free modification (bad practice) 2-doesn't allow the user to do any custom code when the list changes (such as what you could do if the setter was called). It might be possible to make an observable collection and listen to it, but this is a much more complicated workaround than just calling the setter method.

Sergey Ponomarev
  • 2,947
  • 1
  • 33
  • 43
Coder
  • 1,375
  • 2
  • 20
  • 45

1 Answers1

2

Background

When a collection property is mapped in JAXB it first checks the getter to see if the collection property has been pre-initialized. In the example below I want to have my property exposed as List<String>, but have the backing implementation be a LinkedList ready to hold 1000 items.

private List<String> foos = new LinkedList<String>(1000);

@XmlElement(name="foo")
public List<String> getFoos() {
    return foos;
}

Why Your Code Used to Work

If you previously had JAXB call the setter on a property mapped to a collection that returned a non-null response from the getter, then there was a bug in that JAXB implementation. Your code should not have worked in the previous version either.

How to Get the Setter Called

To have the setter called you just need to have your getter return null, on a new instance of the object. Your code could look something like:

import java.util.*;
import javax.xml.bind.annotation.*;

@XmlRootElement(name = "Foo")
public class Foo {

    private List<CustomObject> list = null;

    @XmlElementWrapper(name = "Wrap")
    @XmlElement(name = "Item", required = true)
    public synchronized void setList(List<CustomObject> values) {
        if (null == list) {
            list = new ArrayList<CustomObject>();
        } else {
            list.clear();
        }
        list.addAll(values);
    }

    public synchronized List<CustomObject> getList() {
        if (null == list) {
            return null;
        }
        return new ArrayList(list);
    }

}

UPDATE

If you don't need to perform any logic on the List returned from JAXB's unmarshalling then using field access may be an acceptable solution.

@XmlRootElement(name = "Foo")
@XmlAccessorType(XmlAccessType.FIELD)
public class Foo {

    @XmlElementWrapper(name = "Wrap")
    @XmlElement(name = "Item", required = true)
    private List<CustomObject> list = null;

    public synchronized void setList(List<CustomObject> values) {
        if(null == list) {
            list = new ArrayList<CustomObject>();
        } else {
            list.clear();
        }
        list.addAll(values);
    }

    public synchronized List<CustomObject> getList() {
        return new ArrayList(list);
    }

}
Community
  • 1
  • 1
bdoughan
  • 147,609
  • 23
  • 300
  • 400
  • 1
    Thanks for the explanation. I really appreciate your help. My code definitely worked in the previous version and I don't think JAXB should be making those assumptions. In my case i did initialize a new empty list in the instance variable and this was to avoid null checking everywhere as I did not allow setting the list to null. It seems that this way I will have to create a second fake getter and setter and set them to private(if this will even work) just for JAXB and return null if the list is empty. Why would JAXB do this.It should unconditionally call the setter if i have it annotated – Coder May 15 '14 at 13:08
  • Are you sure that the xml will be loaded correctly if you return a null list from the getter because return new ArrayList(list); did not load the xml (it was saved just wasn't loaded). I'm pretty sure returning null will result in not loading that property even if the setter gets called. – Coder May 15 '14 at 13:27
  • @Coder - You should see the setter called with a populated list. Then what are you doing in your setter? – bdoughan May 15 '14 at 14:19
  • clearing the current list and adding all the values as shown in my code above, however the list passed to me was empty even though it was saved in the xml. I'm not sure if you can try a simple example because it didn't seem to work for me. – Coder May 15 '14 at 15:34
  • It seems like the only way to get this to work correctly with the setter and still loading without exposing the internal list variable is by doing what is mentioned in this link: http://stackoverflow.com/questions/14986562/how-does-unmarshalling-work-in-jaxb see the post by answered Feb 20 '13 at 20:05 chama – Coder May 15 '14 at 15:54
  • @Coder - I have updated my answer with some additional information that may help. – bdoughan May 15 '14 at 19:14
  • I have no idea how you are able to get the code in your update to work. I implemented a test that does the same thing and the values list passed to the setter has size 0 from JAXB. If this worked it would be acceptable, but it just passes an empty list. – Coder May 15 '14 at 21:20
  • If I have my getter method and setter methods exactly the way you have them (but i have the annotations on the setter instead of the field), my setter doesn't even get called. If instead i check if list is empty in the getter and if it is, i return a null value then my setter gets called but with a blank list as a parameter instead of the values from the xml. – Coder May 15 '14 at 21:29
  • Just to make doubly sure, i took the exact code you provided, changed CustomObject to String for simplicity and tried saving and loading it and the setter wasn't called at all during the load. – Coder May 15 '14 at 21:35
  • @Coder - Did you try the approach with field access? – bdoughan May 16 '14 at 18:23
  • Like I mentioned, I tried your exact example and I couldn't get it to work. Did you try your example? – Coder May 20 '14 at 19:21