简体   繁体   中英

Handle multiple versions of XSD generated classes

I'm writing an application that will integrate towards an API that has endpoints that return XML backed by XSDs. My application will have to initially target two different versions of this (perhaps more in future releases), and this does not have to be dynamic. When you start the application, the user will have to tell the application which API major version to support. The XSDs are not under my control, I do not want to edit them.

The XSDs generate classes of the same name, and I've run into problems there already. I can't load both of the ObjectFactory classes generated by XJC into a JAXBContext. My solution to this is right now a map of JAXBContexts:

private static Map<Integer, Pair<Class<?>, JAXBContext>> contexts = new HashMap<>();

static {
    try {
        contexts.put(4, Pair.of(com.api.v4_2_0.ObjectFactory.class, JAXBContext.newInstance(com.api.v4_2_0.ObjectFactory.class)));
        contexts.put(3, Pair.of(com.api.v3_9_4.ObjectFactory.class, JAXBContext.newInstance(com.api.v3_9_4.ObjectFactory.class)));
    } catch (JAXBException e) {
        LOGGER.error("Failed to initialize JAXBContext", e);
    }
}

The pair is used to know which class the JAXBContext is based on, since I can't recover that in runtime. Then, to serialize an object I use a lot of magic reflection which works but doesn't feel right:

public static String objectToString(final Object object, final Integer majorVersion) {
    try {
        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        getMarshallerForMajorVersion(majorVersion).marshal(createJaxbElementFromObject(object, getObjectFactoryForMajorVersion(majorVersion)), os);
        return os.toString(UTF_8);
    } catch (JAXBException e) {
        throw SesameException.from("Failed to serialize object", e);
    }
}

private static Marshaller getMarshallerForMajorVersion(final Integer majorVersion) throws JAXBException {
    return getContextForMajorVersion(majorVersion).getRight().createMarshaller();
}

private static Class<?> getObjectFactoryForMajorVersion(final Integer majorVersion) {
    return getContextForMajorVersion(majorVersion).getLeft();
}

private static Pair<Class<?>, JAXBContext> getContextForMajorVersion(final Integer majorVersion) {
    if (contexts.containsKey(majorVersion)) {
        return contexts.get(majorVersion);
    }
    throw illegalArgument("No JAXBContext for API with major version %d", majorVersion);
}

private static JAXBElement<?> createJaxbElementFromObject(final Object object, final Class<?> objectFactory) {
    try {
        LOGGER.info("Attempting to find a JAXBElement producer for class {}", object.getClass());
        final Method method = findElementMethodInObjectFactory(object, objectFactory);
        return (JAXBElement<?>) method.invoke(objectFactory.getConstructor().newInstance(), object);
    } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException e) {
        throw illegalArgument("Failed to construct JAXBElement for class %s", object.getClass().getName());
    }
}

private static Method findElementMethodInObjectFactory(final Object object, final Class<?> left) {
    return Arrays.stream(left.getMethods())
            .filter(m -> m.getReturnType().equals(JAXBElement.class))
            .filter(m -> m.getName().endsWith(object.getClass().getSimpleName()))
            .findFirst()
            .orElseThrow(() -> illegalArgument("Failed to find JAXBElement constructor for class %s", object.getClass().getName()));
}

This works fine but feels fragile.

The problem

Where it gets worse is having to deserialize XML into an object using generics:

public static <T> T stringToObject(final String xml, final Class<T> toClass, final Integer majorVersion) {
    try {
        final Unmarshaller unmarshaller = getUnmarshallerForVersion(majorVersion);
        final JAXBElement<T> unmarshalledElement = (JAXBElement<T>) unmarshaller.unmarshal(new StringReader(xml));
        return toClass.cast(unmarshalledElement.getValue());
    } catch (JAXBException e) {
        throw SesameException.from(format("Failed to deserialize XML into %s", toClass.getCanonicalName()), e);
    }
}

// And calling this from another class
private com.api.v4_2_0.SomeClass.class toSomeClass(final HttpResponse<String> response) {
    return XmlUtil.stringToObject(response.body(), com.api.v4_2_0.SomeClass.class, apiMajorVersion); // <--- I can't beforehand use this package since major version might be 3.
}

Now there is (from my knowledge) no way for me to use generics and map this to the correct class in the correct package based on what major version of the API is used.

I've also tried using an abstract base class and just giving it a single ObjectFactory for each version, but that still gives me the issue describe here in the problem section. I don't know how to return the correct version of the class:

private com.api.v4_2_0.SomeClass.class toSomeClass(final HttpResponse<String> response) {
    return version4XmlUtil.stringToObject(response.body(), com.api.v4_2_0.SomeClass.class); // <--- I can't beforehand use this package since major version might be 3.
}

How do I structure my code to solve this problem? What patterns are useful? Am I going down the wrong path entirely?

EclipseLink JAXB (MOXy)'s @XmlPath and external binding file extensions can help.

The blog I cite below maps a single object model to two different XML schemas. It does this by mapping the first API by using a combination of standard JAXB and MOXy extension annotations.

package blog.weather;

import java.util.List;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

import org.eclipse.persistence.oxm.annotations.XmlPath;

@XmlRootElement(name="xml_api_reply")
@XmlType(propOrder={"location", "currentCondition", "currentTemperature", "forecast"})
@XmlAccessorType(XmlAccessType.FIELD)
public class WeatherReport {

    @XmlPath("weather/forecast_information/city/@data")
    private String location;

    @XmlPath("weather/current_conditions/temp_f/@data")
    private int currentTemperature;

    @XmlPath("weather/current_conditions/condition/@data")
    private String currentCondition;

    @XmlPath("weather/forecast_conditions")
    private List<Forecast> forecast;

}

And then...

package blog.weather;

import org.eclipse.persistence.oxm.annotations.XmlPath;

public class Forecast {

    @XmlPath("day_of_week/@data")
    private String dayOfTheWeek;

    @XmlPath("low/@data")
    private int low;

    @XmlPath("high/@data")
    private int high;

    @XmlPath("condition/@data")
    private String condition;

}

You can't create a secondary set of mappings to an object model by annotations, so additional ones can utilize MOXy's XML metadata, thus covering the second API.

<?xml version="1.0"?>
<xml-bindings
    xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="blog.weather"
    xml-mapping-metadata-complete="true">
    <xml-schema element-form-default="QUALIFIED">
        <xml-ns prefix="yweather" namespace-uri="http://xml.weather.yahoo.com/ns/rss/1.0"/>
    </xml-schema>
    <java-types>
        <java-type name="WeatherReport" xml-accessor-type="FIELD">
            <xml-root-element name="rss"/>
            <xml-type prop-order="location currentTemperature currentCondition forecast"/>
            <java-attributes>
                <xml-attribute java-attribute="location" xml-path="channel/yweather:location/@city"/>
                <xml-attribute java-attribute="currentTemperature" name="channel/item/yweather:condition/@temp"/>
                <xml-attribute java-attribute="currentCondition" name="channel/item/yweather:condition/@text"/>
                <xml-element java-attribute="forecast" name="channel/item/yweather:forecast"/>
            </java-attributes>
        </java-type>
        <java-type name="Forecast" xml-accessor-type="FIELD">
            <java-attributes>
                <xml-attribute java-attribute="dayOfTheWeek" name="day"/>
                <xml-attribute java-attribute="low"/>
                <xml-attribute java-attribute="high"/>
                <xml-attribute java-attribute="condition" name="text"/>
            </java-attributes>
        </java-type>
    </java-types>
</xml-bindings>

Default behavior is that MOXy's mapping document is used to augment any annotations specified on the model. Depending on how you set the xml-mapping-metadata-complete flag, however, the XML metadata can either completely replace , or simply augment (default), the metadata provided by annotations.

Try it on for size, and let me know what you think:

Mapping Objects to Multiple XML Schemas Using EclipseLink MOXy http://blog.bdoughan.com/2011/09/mapping-objects-to-multiple-xml-schemas.html

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM