问题
I have a fixed XML structure that is already utilised by other applications. Some of these are third party, so changing the XML is not an option.
The XML contains a section which I am struggling to unmarshal. Below is a cut down version. This element is a child of other elements.
<premium>
<allowInstalments>true</allowInstalments>
<annualPremium>2964.23</annualPremium>
<!-- other various elements -->
<calcElement partname="driver">
<driverXs>300.00</driverXs>
<seq>1</seq>
</calcElement>
<calcElement partname="ratingData">
<baseMiles>6000</baseMiles>
<vehicleGroup>15</vehicleGroup>
<documentVersion>4</documentVersion>
</calcElement>
</premium>
To test that this unmarshals correctly (and marshal, but I'm trying to unmarshal at the moment), I have written the following test:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RegressionApplication.class})
public class AdaptedCalcElementTest {
@Autowired
@Qualifier(value = "unmarshaller")
private Unmarshaller unmarshaller;
@Test
public void canUnmarshallIntoDriverCalcElement() throws Exception {
String xml = "<wrapper><calcElement partname=\"driver\">" +
"<driverXs>300.00</driverXs>" +
"<seq>1</seq>" +
"</calcElement></wrapper>";
CalcElementWrapper calcElementWrapper = (CalcElementWrapper) unmarshaller.unmarshal(Input.from(xml).build());
assertThat(calcElementWrapper, notNullValue());
assertThat(calcElementWrapper.listElements, notNullValue());
assertThat(calcElementWrapper.listElements, hasSize(1));
CalcElement calcElement = calcElementWrapper.listElements.get(0);
assertThat(calcElement, instanceOf(DriverCalcElement.class));
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "wrapper")
public static class CalcElementWrapper {
@XmlJavaTypeAdapter(CalcElementAdapter.class)
public List<CalcElement> listElements;
}
}
My Adapter class creates the correct CalcElement extended class based on the pathname attribute:
public class CalcElementAdapter extends XmlAdapter<CalcElementAdapter.AdaptedCalcElement, CalcElement> {
@Override
public CalcElement unmarshal(CalcElementAdapter.AdaptedCalcElement v) throws Exception {
if (v.partname.equalsIgnoreCase("driver")) {
DriverCalcElement calcElement = new DriverCalcElement();
calcElement.setPartname(v.partname);
calcElement.setDriverXs(new BigDecimal(v.driverXs));
calcElement.setSeq(new Integer(v.seq));
return calcElement;
} else if (v.partname.equalsIgnoreCase("ratingData")) {
RatingDataCalcElement calcElement = new RatingDataCalcElement();
calcElement.setBaseMiles(new Integer(v.baseMiles));
calcElement.setDocumentVersion(new Integer(v.documentVersion));
calcElement.setVehicleGroup(new Integer(v.vehicleGroup));
return calcElement;
}
return null;
}
@Override
public CalcElementAdapter.AdaptedCalcElement marshal(CalcElement v) throws Exception {
return null;
}
public static class AdaptedCalcElement {
@XmlAttribute
public String partname;
public String driverXs;
public String seq;
public String baseMiles;
public String vehicleGroup;
public String documentVersion;
}
}
And the CalcElement and derived classes are defined as follow:
public abstract class CalcElement {
private String partname;
@XmlAttribute
public String getPartname() {
return partname;
}
public void setPartname(String partname) {
this.partname = partname;
}
}
public class DriverCalcElement extends CalcElement {
private BigDecimal driverXs;
private Integer seq;
public BigDecimal getDriverXs() {
return driverXs;
}
public void setDriverXs(BigDecimal driverXs) {
this.driverXs = driverXs;
}
public Integer getSeq() {
return seq;
}
public void setSeq(Integer seq) {
this.seq = seq;
}
}
public class RatingDataCalcElement extends CalcElement {
private Integer baseMiles;
private Integer vehicleGroup;
private Integer documentVersion;
public Integer getBaseMiles() {
return baseMiles;
}
public void setBaseMiles(Integer baseMiles) {
this.baseMiles = baseMiles;
}
public Integer getVehicleGroup() {
return vehicleGroup;
}
public void setVehicleGroup(Integer vehicleGroup) {
this.vehicleGroup = vehicleGroup;
}
public Integer getDocumentVersion() {
return documentVersion;
}
public void setDocumentVersion(Integer documentVersion) {
this.documentVersion = documentVersion;
}
}
The unmarshaller is configured as the following:
@Bean(name = "unmarshaller")
Unmarshaller getUnmarshaller() {
return getJaxb2Marshaller();
}
private Jaxb2Marshaller getJaxb2Marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setPackagesToScan("com.itb.lego.regression");
return marshaller;
}
I tried to follow a blog posting of @blaise-doughan, but I can't see what I am missing.
However, the test is failing because it is not calling the adapter for the calcElement elements. What am I missing?
回答1:
I found that I had to go a lot further than just to tell JaxB how to unmarshal the CalcElement.
My first step was to create a class which could be unmarshalled from any type of CalcElement.
@XmlRootElement(name = "calcElement")
public class AdaptableCalcElement {
@XmlAttribute
public String partname;
@XmlElement
public String driverXs;
@XmlElement
public String seq;
@XmlElement
public String baseMiles;
@XmlElement
public String vehicleGroup;
@XmlElement
public String documentVersion;
}
All elements are strings, because this is just a transitional class, but will hold all of the available data to allow me to write logic to create the specific CalcElement derived classes.
Because the calcElement elements are not wrapped in a containing element, and the difference between the two containing classes is the immediate child element. This means that I needed 2 different wrapper classes. One to hold the list of AdaptableCalcElement and then the final version to hold a list of CalcElement.
This gave me the following 2 classes:
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlRootElement(name = "premium")
public class AdaptablePremium extends BasePremium {
private List<AdaptableCalcElement> calcElements;
@XmlElement(name = "calcElement")
public List<AdaptableCalcElement> getCalcElements() {
return calcElements;
}
public void setCalcElements(List<AdaptableCalcElement> calcElements) {
this.calcElements = calcElements;
}
}
and
public class Premium extends BasePremium {
List<CalcElement> calcElements;
public List<CalcElement> getCalcElements() {
return calcElements;
}
public void setCalcElements(List<CalcElement> calcElements) {
this.calcElements = calcElements;
}
}
both of these are extended from BasePremium, which contains all the fields which are common to both.
At this point I can unmarshal the XML into AdaptablePremium. The premium XML is only valid within a containing XML, which allows us to specify that the container (Details) contains Premium, and how to unmarshal it.
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "detail")
public class Detail {
// All other fields for this class
@XmlJavaTypeAdapter(PremiumAdapter.class)
private Premium premium;
}
This now means that when I unmarshal a detail element, it will use this class and attempt to unmarshal the premium element using the PremiumAdapter class. The PremiumAdapter class defines how to convert an AdaptablePremium into a Premium. BasePremium has a lot of members (very poor XML structure, but it is fixed), so rather than copying each member I used reflection to copy everything except the CalcElements and then did that explicitly. I was left with the following XmlAdapter class
@Component
public class PremiumAdapter extends XmlAdapter<AdaptablePremium, Premium> {
@Override
public Premium unmarshal(AdaptablePremium adaptablePremium) throws Exception {
Premium premium = new Premium();
Class<?> premiumClass = premium.getClass();
Class<?> adaptablePremiumClass = adaptablePremium.getClass();
ReflectionUtils.doWithFields(Premium.class,
field -> {
String fieldName = capitalise(field.getName());
Class<?> type = field.getType();
try {
Method getter = adaptablePremiumClass.getMethod("get" + fieldName);
Method setter = premiumClass.getMethod("set" + fieldName, type);
String value = (String) ReflectionUtils.invokeMethod(getter, adaptablePremium);
if (type.equals(String.class)) {
ReflectionUtils.invokeMethod(setter, premium, value);
} else if (type.equals(BigDecimal.class)) {
ReflectionUtils.invokeMethod(setter, premium, new BigDecimal(value));
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
},
field -> !field.getName().equalsIgnoreCase("calcElements")
);
adaptCalcElements(adaptablePremium, premium);
return premium;
}
public String capitalise(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
public void adaptCalcElements(AdaptablePremium adaptablePremium, Premium premium) {
List<CalcElement> calcElements = new ArrayList<>();
for(AdaptableCalcElement adaptableCalcElement : adaptablePremium.getCalcElements()) {
if (adaptableCalcElement.partname.equalsIgnoreCase("driver")) {
DriverCalcElement calcElement = new DriverCalcElement();
calcElement.setPartname(adaptableCalcElement.partname);
calcElement.setDriverXs(new BigDecimal(adaptableCalcElement.driverXs));
calcElement.setSeq(new Integer(adaptableCalcElement.seq));
calcElements.add(calcElement);
} else if (adaptableCalcElement.partname.equalsIgnoreCase("ratingData")) {
RatingDataCalcElement calcElement = new RatingDataCalcElement();
calcElement.setPartname(adaptableCalcElement.partname);
calcElement.setBaseMiles(new Integer(adaptableCalcElement.baseMiles));
calcElement.setDocumentVersion(new Integer(adaptableCalcElement.documentVersion));
calcElement.setVehicleGroup(new Integer(adaptableCalcElement.vehicleGroup));
calcElements.add(calcElement);
}
}
premium.setCalcElements(calcElements);
}
@Override
public AdaptablePremium marshal(Premium v) throws Exception {
// Currently unimplemented
return null;
}
}
I have some further refactoring to do, but this will now correctly unmarshal the xml into the correct types.
来源:https://stackoverflow.com/questions/35012155/xmljavatypeadapter-not-calling-typeadapter