简体   繁体   English

处理多个版本的 XSD 生成类

[英]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.我正在编写一个应用程序,该应用程序将集成到一个 API,该 API 具有返回 XSD 支持的 XML 的端点。 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.当您启动应用程序时,用户必须告诉应用程序支持哪个 API 主要版本。 The XSDs are not under my control, I do not want to edit them. XSD 不在我的控制之下,我不想编辑它们。

The XSDs generate classes of the same name, and I've run into problems there already. XSD 生成同名的类,我已经在那里遇到了问题。 I can't load both of the ObjectFactory classes generated by XJC into a JAXBContext.我无法将 XJC 生成的两个ObjectFactory类加载到 JAXBContext 中。 My solution to this is right now a map of JAXBContexts:我的解决方案现在是 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.这对用于知道 JAXBContext 基于哪个类,因为我无法在运行时恢复它。 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:更糟糕的是必须使用泛型将 XML 反序列化为对象:

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.现在(据我所知)我无法使用泛型并根据使用的 API 的主要版本将其映射到正确包中的正确类。

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.我也尝试过使用抽象基类,并为每个版本只给它一个ObjectFactory ,但这仍然给了我在问题部分中描述的问题。 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. EclipseLink JAXB (MOXy) 的 @XmlPath 和外部绑定文件扩展可以提供帮助。

The blog I cite below maps a single object model to two different XML schemas.我在下面引用的博客将单个对象模型映射到两个不同的 XML 模式。 It does this by mapping the first API by using a combination of standard JAXB and MOXy extension annotations.它通过使用标准 JAXB 和 MOXy 扩展注释的组合映射第一个 API 来实现这一点。

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.您无法通过注解创建到对象模型的第二组映射,因此其他映射可以利用 MOXy 的 XML 元数据,从而覆盖第二个 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.默认行为是使用 MOXy 的映射文档来扩充模型上指定的任何注释。 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.然而,根据您设置xml-mapping-metadata-complete标志的方式,XML 元数据可以完全替换简单地增加(默认)注释提供的元数据。

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 使用 EclipseLink MOXy 将对象映射到多个 XML 模式http://blog.bdoughan.com/2011/09/mapping-objects-to-multiple-xml-schemas.html

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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