castor
  1. castor
  2. CASTOR-3057

Castor should infer types when unmarshalling

    Details

    • Type: Improvement Improvement
    • Status: Open Open
    • Priority: Major Major
    • Resolution: Unresolved
    • Affects Version/s: 1.3.2
    • Fix Version/s: None
    • Component/s: XML
    • Labels:
      None
    • Number of attachments :
      1

      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)

        Activity

        Hide
        Craig Tataryn added a comment -

        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.

        public class CastorMarshallerOverrides {
            
            private static ThreadLocal<Class> unMarshalTo = new ThreadLocal<Class>() {
                protected synchronized Class initialValue() {
                    return null;
                }
            };
           
            /**
             * This will be the Class which the root element of the XML document will be
             * unmarshalled to
             * @return class type for the object which will be returned by {@link Unmarshaller#unmarshal(org.xml.sax.InputSource)}
             */
            public static Class getUnmarshalTo() {
                return CastorMarshallerOverrides.unMarshalTo.get();
            }
            
            /**
             * Override Castor's default choice for which Class to unmarshal an XML document to
             * @param classToUse the Class to use for the Object returned by {@link Unmarshaller#unmarshal(org.xml.sax.InputSource)}
             */
            public static void setUnmarshalTo(Class classToUse) {
                CastorMarshallerOverrides.unMarshalTo.set(classToUse);
            }
        }
        

        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.

        public class ControllerInterceptor extends HandlerInterceptorAdapter {
        
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                if (handler == null) return true;
                String path = request.getPathInfo();
                
                if (MyController.class.isAssignableFrom(handler.getClass())) {
                    if ("/some/uri".equals(path)) {
                        //example where you want to unmarshal to the Child, not the Parent
                        CastorMarshallerOverrides.setUnmarshalTo(Child.class);
                    } else if (path.matches("^/some/more/complex/.*/uri/.*$")) {
                        //example where you may want to force ArrayList
                        CastorMarshallerOverrides.setUnmarshalTo(ArrayList.class);
                    }
                    		
                }
                    
                return true;
            }
        
        

        Set that up in your Spring Beans Context file like so:

        	<!-- lets us setup Castor before certain controllers are called -->
        	<bean
        		class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
        		<property name="interceptors">
        			<list>
        				<ref bean="myControllerInterceptor" />
        			</list>
        		</property>
        	</bean>
        	<bean id="myControllerInterceptor" class="com.my.org.ControllerInterceptor" />
        
        

        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:

        public class CastorXMLContext extends XMLContext {
             //the real XMLContext
             private XMLContext xmlContext;
        
            /**
             * Delegates all XMLContext methods to the delegate passed in
             * 
             * @param xmlContext
             *            object which method calls are delegated to
             */
            public CastorXMLContext(XMLContext xmlContext) {
                this.xmlContext = xmlContext;
            }
            
            /**
             * @param clazz
             * @throws ResolverException
             * @see org.exolab.castor.xml.XMLContext#addClass(java.lang.Class)
             */
            public void addClass(Class clazz) throws ResolverException {
                xmlContext.addClass(clazz);
            }
        
            /**
             * @param clazzes
             * @throws ResolverException
             * @see org.exolab.castor.xml.XMLContext#addClasses(java.lang.Class[])
             */
            public void addClasses(Class[] clazzes) throws ResolverException {
                xmlContext.addClasses(clazzes);
            }
        
            //delegate all methods to the underlying XMLContext except the ones for marshalling/unmarshalling
            //like the examples above
        .
        .
        .
        .
        
            /**
             * @return
             * @see org.exolab.castor.xml.XMLContext#createUnmarshaller()
             */
            public Unmarshaller createUnmarshaller() {
                Unmarshaller unmarshaller = xmlContext.createUnmarshaller();
                // check to see if we have to override Castor's choice for the Class it will use when unmarshalling an XML
                // document to an object
                if (CastorMarshallerOverrides.getUnmarshalTo() != null) {
                    unmarshaller.setClass(CastorMarshallerOverrides.getUnmarshalTo());
                    // clear it for any further marshalling on this thread so it doesn't affect it
                    CastorMarshallerOverrides.setUnmarshalTo(null);
                }
                return unmarshaller;
            }
        
        }
        
        

        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:

        public class CastorMarshaller extends CastorMarshaller {
            private static Logger _LOG = Logger.getLogger(CastorMarshaller.class);
            
            private XMLContext xmlContext;
        
            private Set<String> mappingsAdded = new HashSet<String>();
        
            public CastorMarshaller() {
                super();
            }
            
            public CastorMarshaller(String mappingLocation) {
                super();
                this.setMappingLocation(new ClassPathResource(mappingLocation));
                try {
                    this.afterPropertiesSet();
                } catch (CastorMappingException e) {
                    throw new RuntimeException(e);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            /*
             * Here's where we instruct Spring OXM to use our marshaller instead of its default
             */
            @Override
            protected XMLContext createXMLContext(Resource[] mappingLocations, Class[] targetClasses, String[] targetPackages)
                    throws MappingException, IOException, ResolverException {
                this.xmlContext = new CastorXMLContext(super.createXMLContext(mappingLocations, targetClasses, targetPackages));
                return this.xmlContext;
            }
        
        }
        
        

        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):

        @RequestMapping("/some/path/{id}")
        public @ResponseBody List<Employee> getEmployees(@PathVariable Integer id) {
            //sets the marshalled element name for the List returned to "EmployeeList" instead of Castor default of "array-list"
            CastorMarshallerOverrides.setRootElementName("EmployeeList");
            return employeeService.getEmployee(id);
        }
        

        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.

        Show
        Craig Tataryn added a comment - 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. public class CastorMarshallerOverrides { private static ThreadLocal< Class > unMarshalTo = new ThreadLocal< Class >() { protected synchronized Class initialValue() { return null ; } }; /** * This will be the Class which the root element of the XML document will be * unmarshalled to * @ return class type for the object which will be returned by {@link Unmarshaller#unmarshal(org.xml.sax.InputSource)} */ public static Class getUnmarshalTo() { return CastorMarshallerOverrides.unMarshalTo.get(); } /** * Override Castor's default choice for which Class to unmarshal an XML document to * @param classToUse the Class to use for the Object returned by {@link Unmarshaller#unmarshal(org.xml.sax.InputSource)} */ public static void setUnmarshalTo( Class classToUse) { CastorMarshallerOverrides.unMarshalTo.set(classToUse); } } 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. public class ControllerInterceptor extends HandlerInterceptorAdapter { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler == null ) return true ; String path = request.getPathInfo(); if (MyController.class.isAssignableFrom(handler.getClass())) { if ( "/some/uri" .equals(path)) { //example where you want to unmarshal to the Child, not the Parent CastorMarshallerOverrides.setUnmarshalTo(Child.class); } else if (path.matches( "^/some/more/complex/.*/uri/.*$" )) { //example where you may want to force ArrayList CastorMarshallerOverrides.setUnmarshalTo(ArrayList.class); } } return true ; } Set that up in your Spring Beans Context file like so: <!-- lets us setup Castor before certain controllers are called --> <bean class= "org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" > <property name= "interceptors" > <list> <ref bean= "myControllerInterceptor" /> </list> </property> </bean> <bean id= "myControllerInterceptor" class= "com.my.org.ControllerInterceptor" /> 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: public class CastorXMLContext extends XMLContext { //the real XMLContext private XMLContext xmlContext; /** * Delegates all XMLContext methods to the delegate passed in * * @param xmlContext * object which method calls are delegated to */ public CastorXMLContext(XMLContext xmlContext) { this .xmlContext = xmlContext; } /** * @param clazz * @ throws ResolverException * @see org.exolab.castor.xml.XMLContext#addClass(java.lang. Class ) */ public void addClass( Class clazz) throws ResolverException { xmlContext.addClass(clazz); } /** * @param clazzes * @ throws ResolverException * @see org.exolab.castor.xml.XMLContext#addClasses(java.lang. Class []) */ public void addClasses( Class [] clazzes) throws ResolverException { xmlContext.addClasses(clazzes); } //delegate all methods to the underlying XMLContext except the ones for marshalling/unmarshalling //like the examples above . . . . /** * @ return * @see org.exolab.castor.xml.XMLContext#createUnmarshaller() */ public Unmarshaller createUnmarshaller() { Unmarshaller unmarshaller = xmlContext.createUnmarshaller(); // check to see if we have to override Castor's choice for the Class it will use when unmarshalling an XML // document to an object if (CastorMarshallerOverrides.getUnmarshalTo() != null ) { unmarshaller.setClass(CastorMarshallerOverrides.getUnmarshalTo()); // clear it for any further marshalling on this thread so it doesn't affect it CastorMarshallerOverrides.setUnmarshalTo( null ); } return unmarshaller; } } 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: public class CastorMarshaller extends CastorMarshaller { private static Logger _LOG = Logger.getLogger(CastorMarshaller.class); private XMLContext xmlContext; private Set< String > mappingsAdded = new HashSet< String >(); public CastorMarshaller() { super (); } public CastorMarshaller( String mappingLocation) { super (); this .setMappingLocation( new ClassPathResource(mappingLocation)); try { this .afterPropertiesSet(); } catch (CastorMappingException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } /* * Here's where we instruct Spring OXM to use our marshaller instead of its default */ @Override protected XMLContext createXMLContext(Resource[] mappingLocations, Class [] targetClasses, String [] targetPackages) throws MappingException, IOException, ResolverException { this .xmlContext = new CastorXMLContext( super .createXMLContext(mappingLocations, targetClasses, targetPackages)); return this .xmlContext; } } 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): @RequestMapping( "/some/path/{id}" ) public @ResponseBody List<Employee> getEmployees(@PathVariable Integer id) { //sets the marshalled element name for the List returned to "EmployeeList" instead of Castor default of "array-list" CastorMarshallerOverrides.setRootElementName( "EmployeeList" ); return employeeService.getEmployee(id); } 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.
        Hide
        Craig Tataryn added a comment -

        Example Workaround files for both overriding the marshalling/unmarshalling used by the default Spring OXM Castor marshaller

        Show
        Craig Tataryn added a comment - Example Workaround files for both overriding the marshalling/unmarshalling used by the default Spring OXM Castor marshaller
        Hide
        Craig Tataryn added a comment -

        Further to the original problem, when the refactoring of Spring OXM is done for Castor (i.e. the real fix for this, not the workaround proposed), Spring OXM should pass the marshallers the actual type of the parameter which was annotated with @RequestBody. The Unmarshaller can then use that information to determine if the parameter type is instantiable, if it is it should pass the type to: setUnmarshalTo .

        This would involve either either refactoring the Spring OXM API (not just the Castor specific stuff) to add a "Class" parameter which is passed around to the marshaller. Or Castor could write it's own BeanPostProcessor which takes a look for @RequestBody annotations and records the type of the parameter they are attached to in a HashMap keyed by the controller's RequestMapping. We'd have to be able to "know" which RequestMapping invoked the marshaller down within the marshaller though...

        Show
        Craig Tataryn added a comment - Further to the original problem, when the refactoring of Spring OXM is done for Castor (i.e. the real fix for this, not the workaround proposed), Spring OXM should pass the marshallers the actual type of the parameter which was annotated with @RequestBody. The Unmarshaller can then use that information to determine if the parameter type is instantiable, if it is it should pass the type to: setUnmarshalTo . This would involve either either refactoring the Spring OXM API (not just the Castor specific stuff) to add a "Class" parameter which is passed around to the marshaller. Or Castor could write it's own BeanPostProcessor which takes a look for @RequestBody annotations and records the type of the parameter they are attached to in a HashMap keyed by the controller's RequestMapping. We'd have to be able to "know" which RequestMapping invoked the marshaller down within the marshaller though...

          People

          • Assignee:
            Unassigned
            Reporter:
            Craig Tataryn
          • Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

            Dates

            • Created:
              Updated: