Details
-
Type:
Improvement
-
Status:
Open
-
Priority:
Major
-
Resolution: Unresolved
-
Affects Version/s: 1.3.2
-
Fix Version/s: None
-
Component/s: XML
-
Labels:None
-
Number of attachments :
Description
Werner as discussed previously, I'll make an attempt at a patch for this when I have time.
Castor should be smart enough, when unmarshalling, to distinguish which Class to instantiate when more than one <map-to> element in a mapping for two or more Classes have the same "xml" attribute value. Essentially, the unmarshaller should infer teh class to set into: unmarshal.setClass(<most appropriate>.class)
See below for an instance of the problem:
<class name="com.some.domain.Wallet"> <map-to xml="Wallet" /> <!-- Bunch of properties --> </class> <!-- Wallet that has special properties used to facilitate marshalling which is not easily achieved using just the plain Domain objects --> <class name="com.some.domain.WalletFlexFacade" extends="com.some.domain.Wallet"> <map-to xml="Wallet" /> <field name="expenseRequests" collection="collection" type="com.some.domain.flex.ExpenseRequestFlexFacade"> <bind-xml name="ErLine" /> </field> </class>
I then unmarshal some XML that has the <ErLine> elements in them like so:
String _MAPPING = XMLMapper.class.getResource("/resources/mapping/DomainObjectsMapping.xml").toExternalForm(); Mapping _MAP = new Mapping(); _MAP.loadMapping(_MAPPING); InputSource xmlSource = new InputSource(new StringReader(xml)); Unmarshaller unmarshal = new Unmarshaller(_MAP); unmarshal.setValidation(false); Object object = unmarshal.unmarshal(xmlSource);
In the log I get this:
DEBUG - Get descriptor for: com.some.domain.ErLine found: null DEBUG - Now in method: org.exolab.castor.xml.util.resolvers.ByIntrospection resolving: com.some.domain.ErLine DEBUG - Ignored problem at loading class: com.some.domain.ErLine through class loader: sun.misc.Launcher$AppClassLoader@601bb1, exception: java.lang.ClassNotFoundException: com.some.domain.ErLine DEBUG - Called addAllDescriptors with null or empty descriptor map DEBUG - Get descriptor for: com.some.domain.ErLine found: null DEBUG - Adding class name to missing classes: com.some.domain.ErLine DEBUG - unable to find FieldDescriptor for 'ErLine' in ClassDescriptor of Wallet - ignoring extra element. DEBUG - unable to find FieldDescriptor for 'ErLine' in ClassDescriptor of Wallet - ignoring extra element. DEBUG - unable to find FieldDescriptor for 'ErLine' in ClassDescriptor of Wallet - ignoring extra element.
Ok, I think to myself, for some reason Castor can't work backwards from the <bind-xml> statement for the ErLine element to realize that <ErLine> really maps to ExpenseRequestFlexFacade (and the get/setExpenseRequestFacade) when unmarshalling... Also, the "object" being returned from unmarshal.unmarshal(..) ends up being is a Wallet, not a WalletFlexFacade!!!! So what I do is add "setErLine/getErLine" methods to WalletFlexFacade and change the <field> name attribute in the mapping from "expenseRequests" to "erLines" in hopes that Castor will notice that WalletFlexFacade is the right class to instantiate, not Wallet. Nope, that didn't work. It's just hell bent on using "Wallet" because I think it's just the first mapping it found with the proper <map-to> element.
Instead what should happen is when Castor finds two mappings with the same <map-to> xml attribute, it should inventory the differences between the two mappings and determine which mapping is more appropriate for the XML being unmarshalled (i.e. if the XML has a <ErLines> element, and the WalletFlexFacade has a mapping for that but the Wallet doesn't, it should choose the WalletFlexFacade)
A workaround for this is two fold. First, when you want an XML payload to unmarshal to the child type instead of the parent (which Castor seems to pick as the default).
Unmarshalling
The first thing you want to do is setup a ThreadLocal variable where your request thread can store the proper class it should unmarshal the XML Contents.
Now, in order to kick in before Spring OXM you have to write yourself an interceptor. The interceptor will look for certain URIs and determine whether the one requested is one where it needs to override the class to unmarshal to.
Set that up in your Spring Beans Context file like so:
Now you'll need to create your own XMLContext that observes the fact it needs to observe that ThreadLocal variable and set the "setUnmarshalTo" method on the real XMLContext to the value located in the ThreadLocal variable:
So now that you've setup which class you'd like Castor to unmarshal to, you now have to get Spring OXM to observe the changes, you do this by creating your own org.springframework.oxm.castor.CastorMarshaller which uses your custom XMLContext instead of the default Castor one:
Marshalling
Similarly for marshalling to XML node names which differ from those which Castor would pick, you add another ThreadLocal variable in your CastorMarshallerOverrides like so:
private static ThreadLocal<String> rootElementName = new ThreadLocal<String>() { protected synchronized String initialValue() { return ""; } }; . . . /** * The element name which will override Castor's choice for an object marshalled to XML * @return The name to use when marshalling an object to XML */ public static String getRootElementName() { return CastorMarshallerOverrides.rootElementName.get(); } /** * Allows us to override Castor's default element name for objects which are marshalled to XML via {@link Marshaller#marshal(Object)} * @param rootElementName name to use instead of what Castor would have used as an Element name for an object it is marshalling */ public static void setRootElementName(String rootElementName) { CastorMarshallerOverrides.rootElementName.set(rootElementName); }Instead of an interceptor, you'll instead set this ThreadLocal variable prior to leaving your controller method (and prior to Spring OXM marshalling):
And we must override the custom XMLContext's createMarshaller() method to observe the ThreadLocal var:
public Marshaller createMarshaller() { Marshaller marshaller = xmlContext.createMarshaller(); // check to see if we have to override Castor's choice of the root XML element name to use when marshalling // and object to XML if (StringUtils.trimToNull(CastorMarshallerOverrides.getRootElementName()) != null) { marshaller.setRootElement(CastorMarshallerOverrides.getRootElementName()); // clear it for any further marshalling on this thread so it doesn't affect it CastorMarshallerOverrides.setRootElementName(null); } return marshaller; }That's pretty much it, attached is an archive of the code examples for convenience sake.