Reputation: 15225
I've started to use Commons JXPath in my unit and integration tests, as a simple way to set a set of properties in a bean hierarchy.
One scenario where I can't use JXPath "cleanly" is in setting list entries.
I'd really like for JXPath to do the "obvious stuff", so that if I try to set entry "[1]" of a list (odd that they chose to use 1-based indexes), it will configure the underlying list to have at least one entry. From what I've seen, this doesn't happen, and I have to do it manually.
For instance, here's some code that I'd like to write:
JXPathContext context = JXPathContext.newContext(request);
context.setValue("foo", new Foo());
context.setValue("foo/subscriberNumber", "1234567890");
context.setValue("foo/bar", new Bar());
context.setValue("foo/bar/numbers[1]", "123"); // this fails
This does not work, for a couple of different reasons.
The first issue is that the "numbers" list property will be null. I could deal with that like this:
JXPathContext context = JXPathContext.newContext(request);
context.setValue("foo", new Foo());
context.setValue("foo/subscriberNumber", "1234567890");
context.setValue("foo/bar", new Bar());
context.setValue("foo/bar/numbers", new ArrayList<String>());
context.setValue("foo/bar/numbers[1]", "123");
Or I could register an AbstractFactory and use "createPathAndSetValue()" instead.
However, this still fails because even though the list may exist, it will have zero size and fails to access the 0th entry.
I'm forced to do something like this:
JXPathContext context = JXPathContext.newContext(request);
context.setValue("foo", new Foo());
context.setValue("foo/subscriberNumber", "1234567890");
context.setValue("foo/bar", new Bar());
context.setValue("foo/bar/numbers", Arrays.asList(""));
context.setValue("foo/bar/numbers[1]", "123");
If I have to set other entries in the list besides the first one, I'll have to init the list with more dummy entries.
I'll also feel responsible to add a comment everywhere I do this to quell the WTFs.
Is there a cleaner way to do this?
Upvotes: 1
Views: 1422
Reputation: 1742
What is wrong with:
JXPathContext context = JXPathContext.newContext(request);
context.setValue("foo", new Foo());
context.setValue("foo/subscriberNumber", "1234567890");
context.setValue("foo/bar", new Bar());
context.setValue("foo/bar/numbers", Lists.newArrayList(null, 123, null, ...));
using something like Guava's Lists class ?
Upvotes: 0
Reputation: 373
Hmmm, tricky indeed. First, digressing
(odd that they chose to use 1-based indexes)
That's the way XPath works. There are discussions around why, but I'm not really sure why. Lua programming language also uses 1 for arrays.
Here's some code (also in GitHub) based on your example.
public class TestsJxpath {
public static class Request {
private Foo foo;
public Foo getFoo() {
return foo;
}
public void setFoo(Foo foo) {
this.foo = foo;
}
@Override
public String toString() {
return "Request [foo=" + foo + "]";
}
}
public static class Foo {
private String subscriberNumber;
private Bar bar;
public String getSubscriberNumber() {
return subscriberNumber;
}
public void setSubscriberNumber(String subscriberNumber) {
this.subscriberNumber = subscriberNumber;
}
public Bar getBar() {
return bar;
}
public void setBar(Bar bar) {
this.bar = bar;
}
@Override
public String toString() {
return "Foo [subscriberNumber=" + subscriberNumber + ", bar=" + bar + "]";
}
}
public static class Bar {
private List<String> numbers = Arrays.asList("");
public List<String> getNumbers() {
return numbers;
}
@Override
public String toString() {
return "Bar [numbers=" + numbers + "]";
}
}
// added after OP reported on
// https://stackoverflow.com/questions/44530112/how-to-get-jxpath-to-cleanly-set-list-collection-entries
public static void main(String[] args) {
Request request = new Request();
JXPathContext context = JXPathContext.newContext(request);
context.setValue("foo", new Foo());
context.setValue("foo/subscriberNumber", "1234567890");
context.setValue("foo/bar", new Bar());
context.setValue("foo/bar/numbers[1]", "123"); // this no longer fails
System.out.println(request);
}
public static void main2(String[] args) {
Phone p1 = new Phone(1);
Phone p2 = new Phone(2);
List<Phone> phones = new ArrayList<>();
phones.add(p1);
phones.add(p2);
JXPathContext ctx = JXPathContext.newContext(phones);
System.out.println(phones);
long phone1 = (long) ctx.getValue("/*[type='br.eti.kinoshita.commons.jxpath.Phone']");
System.out.println(phone1);
}
}
When Apache Commons JXPath finds a collection, more specifically, instance of java.util.List, then it will call the set method. No matter what we try. You could try a way around with a factory, or maybe other hacks with static method calls.... but if your case is simple enough that you know beforehand how many elements your list will contain, you could use the approach above, i.e.
private List<String> numbers = Arrays.asList("");
Initialize your list with empty elements, so that List#set(int, E)
doesn't throw an exception.
Hope that helps, Bruno
Upvotes: 0