Announcement

Collapse
No announcement yet.
X
  • Filter
  • Time
Clear All
new posts

    XML translation in DataSource

    Using SmartGWT 2.3, Firefox 3.6.12.

    Hi, I am trying to find a way of binding an XML record (returned from our database as a String) to a dynamic form and some grids for editing, then returning back the modified document to be saved.

    I've been tinkering, and searching the forum and documentation for days without finding a solution, so here goes...

    Currently, I'm trying the approach of creating a Document object built from the XML string in the DataSource and setting it as the data object in the DataSource, but from debugging, it isn't being translated to JSON in the way I was expecting. Simple text nodes seem to convert and render correctly, but any element with attributes is converted into a JSON object and the actual node text is ommitted.

    I've stripped down the problem to a smaller example to demonstrate what I'm seeing. Hopefully someone can shed some light or point me in the right direction:

    Client - (simple DynamicForm in example, but full solution requires ListGrid too for displaying repeating elements in the XML...)
    Code:
    package com.nielsen.smartgwt.test.client;
    
    import com.smartgwt.client.data.DataSource;
    import com.smartgwt.client.widgets.form.DynamicForm;
    import com.smartgwt.client.widgets.form.fields.TextItem;
    import com.smartgwt.client.widgets.layout.VLayout;
    
    
    public class ExampleGUI {
    
        public ExampleGUI() {
            
            VLayout layout = new VLayout();
            DynamicForm form = new DynamicForm();
            TextItem txtArtistID = new TextItem("artistID", "Artist ID");
            TextItem txtArtistSource = new TextItem("artistSource", "Artist Source");
            TextItem txtArtist = new TextItem("artist", "Artist");
            TextItem txtTitle = new TextItem("title", "Title");
            
            form.setDataSource(DataSource.get("exampleDataSource"));
            form.setFields(txtArtistID, txtArtistSource, txtArtist, txtTitle);
            
            layout.addMember(form);
            form.fetchData();
            layout.draw();
        }
    }
    DataSource class - I've removed the handlers/DAOs and just have a static String representing a very cut-down version of the XML.
    I create a Document object using the DocumentBuilder and have verified it's contents by traversing the nodes and outputing what I find.

    Code:
    package com.nielsen.smartgwt.test.server;
    
    import java.io.StringReader;
    
    import javax.xml.parsers.DocumentBuilder;
    import javax.xml.parsers.DocumentBuilderFactory;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.w3c.dom.Document;
    import org.w3c.dom.Element;
    import org.w3c.dom.Node;
    import org.w3c.dom.traversal.DocumentTraversal;
    import org.w3c.dom.traversal.NodeFilter;
    import org.w3c.dom.traversal.NodeIterator;
    import org.xml.sax.InputSource;
    
    import com.isomorphic.datasource.BasicDataSource;
    import com.isomorphic.datasource.DSRequest;
    import com.isomorphic.datasource.DSResponse;
    import com.isomorphic.js.JSTranslater;
    
    public class ExampleDataSource extends BasicDataSource {
        
        private static Log LOG = LogFactory.getLog(ExampleDataSource.class);
        private static DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        static {
            factory.setNamespaceAware(true);    
        }
        private static String xmlHeader = "<?xml version='1.0' encoding='UTF-8'?>";
        private static String xml = 
            "<product id='101'>" +
                "<artist id='224' source='devAdmin'>Artist Name</artist>" +
                "<title>Some Title</title>" +
            "</product>";
        
        /**
         * {@inheritDoc}
         */
        @Override
        public DSResponse executeFetch(DSRequest request) throws Exception {
            LOG.debug("Executing FETCH");
            
            DSResponse dsResponse = new DSResponse();
            dsResponse.setDataSource(this);
            dsResponse.setSuccess();
            dsResponse.setTotalRows(1);
            dsResponse.setStartRow(0);
            dsResponse.setEndRow(0);
            
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(new InputSource(new StringReader(xmlHeader+xml)));
            
            DocumentTraversal traversal = (DocumentTraversal) doc;
    
            NodeIterator iterator = traversal.createNodeIterator(
              doc.getDocumentElement(), NodeFilter.SHOW_ELEMENT, null, true);
            
            for (Node n = iterator.nextNode(); n != null; n = iterator.nextNode()) {
                
                LOG.debug("Name=" + ((Element) n).getTagName() + 
                          " : id=" + ((Element) n).getAttribute("id") +
                          " : source=" + ((Element) n).getAttribute("source") +
                          " : value=" + ((Element) n).getTextContent());
            }
            
            JSTranslater t = JSTranslater.get();
            t.enablePrettyPrinting();
            LOG.debug("Doc to JS=" + t.toJS(doc));
            
            dsResponse.setData(doc);
            return dsResponse;
        }
        
        // ... other methods omitted for brevity
    
    }
    DataSource config file:

    Code:
    <DataSource  
         ID="exampleDataSource"  
         serverConstructor="com.nielsen.smartgwt.test.server.ExampleDataSource"
     >
        <fields>
            <field name="artistID" type="integer" valueXPath="artist[@id]" />
            <field name="artistSource" type="text" valueXPath="artist[@source]" />
            <field name="artist" type="text" valueXPath="artist"/>
            <field name="title" title="Title" type="text" valueXPath="title" />
        </fields>
    </DataSource>
    The valueXPath declarations seem to be causing the DataSource to create JSON object structures for 'artist', 'artistID' and 'artistSource' fields. However none of these objects contain the actual XML node text. (Why?)
    If I omit the valueXPath value from each field, the DataSource creates just one object 'artist' with two attributes 'id' and 'source', (which is more what I would expect) but again, no data for the actual tag text.

    Does this mean the xpath values in the DataSource config are irrelevant? I can see the client just maps the JSON fields using the name? (Which is why "title" gets rendered correctly and the others get rendered as "[object Object]"). But if I change the DataSource config to set dataFormat="xml" or add a recordXPath="/product" statement, an error is thrown when the DataSource is initialized.

    RPC Response when fetch is called with valueXPath values applied to fields in the config file:
    Code:
    //isc_RPCResponseStart-->[{endRow:0,totalRows:1,isDSResponse:true,invalidateCache:false,status:0,startRow:0,data:{title:"Some Title",artistID:{id:"224",source:"devAdmin"},artistSource:{id:"224",source:"devAdmin"},artist:{id:"224",source:"devAdmin"},id:"101"}}]//isc_RPCResponseEnd
    I've tried a few variations on this theme, tweaking the DataSource config with various options with no success - my main question is, is this the right approach for this problem or not? If not, can anyone suggest an alternative approach that will allow us to easily pass a XML String/Document to the client, and get a (modified) String/Document back (preferably with examples!?) Will I have problems rendering elements in a ListGrid too?

    If the general approach is correct, can anyone fill-in the blanks... what configuration do I need in my DataSource and how can I get my Document object to translate correctly? I've looked at the JSTranslater javadoc but calling toJS() on the Document object isn't yeilding the result I was expecting. i.e. it omits the text values of the tags that have attributes.
    The javadoc states that
    Code:
    XML Elements (org.w3c.dom.Element) and XML Documents (org.w3c.dom.Document) are treated identically to XML found in <isomorphic:XML> tags in .jsps, that is, component definitions like <ListGrid .. /> become live SmartClient components, and any other Elements become JavaScript Objects, where each attribute or subelement becomes a JavaScript property.
    but what does it do with the actual tag text?

    Many thanks for any pointers...

    #2
    If you have the ability to integrate at another layer (say, with a Java Beans API), do that instead. This is a messy integration point because there's no schema for how XML messages should be formed. Instead it sounds as if the ultimate data provider is expecting you to hold onto the original XML document and submit the whole thing, even if parts of it weren't modified?

    If so, you can do this, and you're using most of the right APIs, except:

    1. your XPaths aren't quite right, for example, "artist[@id]" selects an artist element with a non-null id attribute, not the id attribute itself

    2. it's expected that if an element is not simple (has attributes or subelements), in the absence of an XPath it will be turned into a JavaScript object containing it's attributes and subelements. There's no other choice except to drop data

    As far as applying updates to the target data, if this is a relatively complex XML document and you're only editing certain properties from it, the best approach might be to store it in the session and apply updates selectively when "update" et al requests come in, rather than trying to round-trip all data to the browser and rebuild the document from scratch.

    Comment


      #3
      Thanks for responding so quickly.

      We do have a schema for the XML and we use JAXB in other areas of the app to marshal/unmarshal the XML and modify data in the document, but as far as I know, we can't pass JAXB objects to the client because they aren't regular POJOs? (Hence I was trying this approach for the client).

      I could write new POJOs and copy the properties from the JAXB objects and pass them to the client, but it's an extra conversion that I was hoping might not be necessary?

      In terms of the xpath, what is the correct way to select the artist id attribute? artist/@id ???

      When you say "in the absence of an xpath", are you talking about a valueXPath in the DataSource config?

      I'd hoped to be able to create a DataSource config something like:
      Code:
      <DataSource  
           ID="exampleDataSource"  
           serverConstructor="com.nielsen.smartgwt.test.server.ExampleDataSource"
           recordXPath="/product"
      >
          <fields>
              <field name="artistID" type="integer" valueXPath="artist/@id" />
              <field name="artistSource" type="text" valueXPath="artist/@source" />
              <field name="artist" type="text" valueXPath="artist" />
              <field name="title" title="Title" type="text" valueXPath="title" />
          </fields>
      </DataSource>
      which could map between the expected xml elements in the Document and the form widgets. Then when the user selects to save the form/grid, the document is rebuilt on the server-side and saved back to the db?

      If I had the correct xpath notation in the DataSource config, could I get the conversion to create JSON like:
      artist:"Artist Name"
      artistID:"101"
      artistSource:"devAdmin" ?

      But I'm still unsure how I go about getting the text value from a non-simple element types because, as you say, it drops the data during conversion. How do I get it to keep the text data as a JSON field?
      Thanks.

      Comment


        #4
        We do have a schema for the XML and we use JAXB in other areas of the app to marshal/unmarshal the XML and modify data in the document, but as far as I know, we can't pass JAXB objects to the client because they aren't regular POJOs? (Hence I was trying this approach for the client).
        If they have getters and setters that follow the Java Beans convention (type getBlah/setBlah(type)) they'll work with DataSources and there's no need to create a separate "DTO" (Data Transfer Object) POJO. See "Returning Data" in the QuickStart Guide chapter on the Server Framework for more information.

        In terms of the xpath, what is the correct way to select the artist id attribute? artist/@id ???
        As mentioned above, you can probably work directly with your JAXB beans, which is much simpler. But yes, the XPath would be either that or artist@id - but the best thing to check is the JXPath reference, that's what server-side valueXPath processing uses. This will also show you how to write an XPath that extracts the text content of a complex element.

        When you say "in the absence of an xpath", are you talking about a valueXPath in the DataSource config?
        Right.

        If I had the correct xpath notation in the DataSource config, could I get the conversion to create JSON like:
        artist:"Artist Name"
        artistID:"101"
        artistSource:"devAdmin" ?
        Yes.

        Comment


          #5
          Thanks Isomorphic, I really appreciate the help.

          Ok, I have now implemented it using the JAXB beans and the valueXPath statements now work the way I expect. My JAXB 'Product' class has an 'Artist' object which has a 'value' attribute, so valueXPath="artist/value" now gets me "Artist Name". (I added productID to check the top-level "id" attribute could be referenced, and it worked too).

          Code:
          <DataSource  
               ID="exampleDataSource"  
               serverConstructor="com.nielsen.smartgwt.test.server.ExampleDataSource"
               mappedBeanClass="com.nielsen.smartgwt.test.jaxb.Product"
           >
              <fields>
                  <field name="productID" type="integer" valueXPath="@id" />
                  <field name="artistID" type="integer" valueXPath="artist/@id" />
                  <field name="artistSource" type="text" valueXPath="artist/@source" />
                  <field name="artist" type="text" valueXPath="artist/value" />
                  <field name="title" type="text" valueXPath="title" />
              </fields>
          </DataSource>
          I was under the impression that the JAXB generated classes would cause an error as they were not just simple DTO's, but they seem to work ok - annotations and all.

          I'm still lost as to how I would write an xpath that would get at the text contents of a complex node though using the SmartGWT classes. I've looked at loads of XPath resources now and asked around the office, but it's still no clearer, there seem to be several ways to do it, none of them that work in my example, e.g. artist/text() artist/. (Am I missing something obvious here???)

          The JSTranslater seems to be a very strict representation of the 'standard', i.e. when creating the JSON object from the Document object it drops the text content of a node if it is a complex node and only converts the attributes. Is there a way of getting it to keep the text content and convert it too?

          Other JSON translators I've looked at will include the text content of a complex node in a JSON field. (Although different translators work in different ways) e.g. If you run the XML string through the org.json.XML.toJSONObject() method, it puts the tag text contents in a JSON field called "contents" (which it defines itself) along with the other named attributes.
          e.g. artist:{id:"224",source:"devAdmin",contents:"Artist Name"}
          (I appreciate I'm comparing a String parser with an object parser here...)

          I'm also curious as to why, if you can specify an XML data file as the source for a DataSource using "dataURL", why can't you do it with an XML String? Or is it that you just can't use complex types in your XML file if you want to access the tag text value?

          Overall, the JAXB approach seems to work. There are some concerns that it will be slower than a more direct DataSource/XML integration as we have to unmarshal the String to an object, then marshal back to a String each time, but at this stage, I just want something that works...!

          :)

          Comment


            #6
            Confused - since you are now working with beans directly why are you still looking at APIs for processing XML - just curiosity?

            JXPath docs suggests that someElement/text() is the correct selector, however, Googling does reveal some people saying it works and some saying it doesn't work.

            As far as preserving the text content when translating to JSON, that's something we do in other contexts (eg client-side WSDL integration) but on the server, you're 1) probably using the wrong approach if you're interacting with XML directly 2) have probably made a mistake if you end up with a complete element in your output data. So, it's not the default, and from what we understand now that you're interacting with beans you shouldn't need this anymore, but you can add a DMI that looks at the dsResponse.getRecords() and grabs the value of the field from each Map, which will be an Element, and replaces that value with the text content.

            Comment


              #7
              It's more curiosity now although I'm still looking because we may run into problems in the future. There is a requirement to process much larger and more complex XML records down the line, so I want to make sure the solution I deliver is scalable and performant.

              I figured I can't be the only person facing a problem like this (i.e. I have XML records that I want to update using a SmartGWT UI) and I wanted to understand the cleanest and simplest approach. If JAXB/DTOs is recommended, then fine, I just thought there might be a more direct integration approach. You use XML files in your client DataSource examples, so I figured there would be an easy way to map XML elements and attributes directly from an XML Document to named fields on the UI in the DataSource config (without manually converting the XML to custom DTOs first). Probably a case of not reading the documentation carefully enough upfront...? ;)

              As for the JSON conversion, what wasn't clear to me was why JSTranslater.toJS(org.w3c.dom.Document) would drop the text content from complex tags and not attempt to create a JSON field like "content" with the text. From other docs I've read, the standard for converting elements to JSON seems to be arbitrary, some converters drop the text because in complex Elements there is effectively no 'named field' to map it to, while others create a special field like "content" and put the text in it.
              e.g. By default, JSTranslater seems to convert the Element <title>Hello</title> to title:"Hello" but <title id="101">Hello</title> converts to title:{id:"101"}

              You alluded to the fact that if I got the right xpath notation in the DataSource config, that I could get the text from the Element and add it to a named field that I specify in my DataSource config. Unfortunately, I'm still none the wiser as to what the correct xpath notation would be, as you've said, after Googling and asking around the generally accepted "someElement/text()" doesn't work and without spending some time debugging what happens in JSTranslater and the DataSource, I doubt I'll find the answer. I'll look at dsResponse.getRecords() anyway.

              Overall though, I really appreciate the pointers, I'm making progress now.
              Many thanks.

              Comment


                #8
                Still confused. If you're working directly with beans then you should not need either server-side XML processing or DTOs (did you read the section in the QuickStart on this?).

                You're asking for the cleanest approach - when you have a Java Object model and you instead work with XML documents that are a serialization of that Java Object model, that's kind of like choosing to work with a CSV dump of a SQL database instead of the database itself. Working with the actual object model is the clean approach.

                As we covered, in other contexts when we convert to JSON we preserve the text content (under the name "xmlTextContent", but the field can be renamed). We don't do it in this case because it's a sign of a bad approach that you would end up in this situation at all.

                If you were forced down this path, and you wanted to get the text content, you would set an XPath that would cause the element itself to be retrieved (which may in this case be no XPath, or just "artist", but you've never shared the XML document itself), then apply the post-processing to the DSResponse that we described.

                But this is neither the recommended nor the cleanest path. Please confirm that you've switched over to working with beans and understand how to avoid the need to use DTOs - we can't keep explaining nuances of an integration approach that we do not recommend.

                Comment


                  #9
                  An example of the XML was included in my first post - I created a static String in the DataSource class to represent it:
                  Code:
                  <product id='101'><artist id='224' source='devAdmin'>Artist Name</artist><title>Some Title</title></product>
                  Sorry, maybe I haven't been clear enough? I understand the best approach would be to have a relational db with a neatly mapped bean-model but that is not what I have to work with. The data I need to manage is stored and retrieved as an XML document, so, when I retrieve the document for updating in the UI, I'm forced to try and generate an object model from it (using the schema and JAXB) to allow me to use SmartGWT the way that has been recommended.

                  I've abandoned any plans to try and process the XML directly as a 'Document' using JSTranslater and I am using the JAXB-generated classes (beans) to try and implement a solution. I merely wanted to understand what the JSTranslater class did with the text data when it converted complex Elements to JSON, because in my original approach, when I debugged the resulting JSON object in Firebug, I didn't see the tag text, only the attributes.

                  Thanks.

                  Comment


                    #10
                    What we thought you were doing is using an XML representation derived from a clean beans object model. Sounds like it's the other way around - you are deriving JAXB beans from an XML message, and you have no more direct way to access the data than the XML message?

                    Regardless, either direct XML usage or JAXB usage in this case is fine. With XML you have the mystery of the apparent JXPath bug with receiving text, but we gave you a workaround for that. With JAXB, so far as we know, you have no issues so far. Given the constraint of working with XML messages from some remote server (as we understand it) the two approaches are about equivalently "clean".

                    Comment

                    Working...
                    X