September 8, 2011

Mapping Objects to Multiple XML Schemas - Weather Example

I have written previous posts on EclipseLink JAXB (MOXy)'s @XmlPath and external binding file extensions.  In this post I will demonstrate how powerful these extensions are by mapping a single object model to two different XML schemas.  To make the example more "real", the XML data will come from two different services that provide weather information:  Google and Yahoo.


Java Model

The following domain model will be used for this post:

Weather Report
package blog.weather;

import java.util.List;

public class WeatherReport {

    private String location;
    private int currentTemperature;
    private String currentCondition;
    private List<Forecast> forecast;

}

Forecast
package blog.weather;

public class Forecast {

    private String dayOfTheWeek;
    private int low;
    private int high;
    private String condition;

}

Google Weather API

First we will leverage Google's Weather API.  The following URL will be used to access the weather data for Ottawa, Canada:


The following is the result of performing the above query at time I was writing this article.  I have highlighted the portions of the XML document that we will map to:

<xml_api_reply version="1">
    <weather module_id="0" tab_id="0" mobile_row="0" mobile_zipped="1"
        row="0" section="0">
        <forecast_information>
            <city data="Ottawa, ON" />
            <postal_code data="Ottawa" />
            <latitude_e6 data="" />
            <longitude_e6 data="" />
            <forecast_date data="2011-09-08" />
            <current_date_time data="2011-09-08 14:00:00 +0000" />
            <unit_system data="US" />
        </forecast_information>
        <current_conditions>
            <condition data="Mostly Cloudy" />
            <temp_f data="66" />
            <temp_c data="19" />
            <humidity data="Humidity: 73%" />
            <icon data="/ig/images/weather/mostly_cloudy.gif" />
            <wind_condition data="Wind: NE at 13 mph" />
        </current_conditions>
        <forecast_conditions>
            <day_of_week data="Thu" />
            <low data="55" />
            <high data="75" />
            <icon data="/ig/images/weather/cloudy.gif" />
            <condition data="Cloudy" />
        </forecast_conditions>
        <forecast_conditions>
            <day_of_week data="Fri" />
            <low data="46" />
            <high data="77" />
            <icon data="/ig/images/weather/mostly_sunny.gif" />
            <condition data="Partly Sunny" />
        </forecast_conditions>
        <forecast_conditions>
            <day_of_week data="Sat" />
            <low data="43" />
            <high data="68" />
            <icon data="/ig/images/weather/sunny.gif" />
            <condition data="Clear" />
        </forecast_conditions>
        <forecast_conditions>
            <day_of_week data="Sun" />
            <low data="55" />
            <high data="75" />
            <icon data="/ig/images/weather/sunny.gif" />
            <condition data="Clear" />
        </forecast_conditions>
    </weather>
</xml_api_reply>

Java Model - Mapped to Google's XML Schema via Annotations

We will map the result of the Google weather API via a combination of standard JAXB and MOXy extension annotations.

Weather Report
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;

}

Forecast
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;

}

Specify MOXy as the JAXB Provider (jaxb.properties)

To configure MOXy as your JAXB provider simply add a file named jaxb.properties in the same package as your domain model with the following entry:

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
For more information see:  Specifying EclipseLink MOXy as Your JAXB Provider.

Demo

The following demo code will read the XML data for Google's weather service, and marshal the objects back to XML:

package blog.weather;

import java.net.URL;
import javax.xml.bind.*;

public class GoogleDemo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(WeatherReport.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        URL url = new URL("http://www.google.com/ig/api?weather=Ottawa");
        WeatherReport weatherReport = (WeatherReport) unmarshaller.unmarshal(url);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(weatherReport, System.out);
    }

}

Output

Below is the result of running the demo code.  The output represents the portion of the XML document that we had mapped to:

<?xml version="1.0" encoding="UTF-8"?>
<xml_api_reply>
   <weather>
      <forecast_information>
         <city data="Ottawa, ON"/>
      </forecast_information>
      <current_conditions>
         <condition data="Mostly Cloudy"/>
         <temp_f data="68"/>
      </current_conditions>
      <forecast_conditions>
         <day_of_week data="Thu"/>
         <low data="55"/>
         <high data="75"/>
         <condition data="Cloudy"/>
      </forecast_conditions>
      <forecast_conditions>
         <day_of_week data="Fri"/>
         <low data="46"/>
         <high data="77"/>
         <condition data="Partly Sunny"/>
      </forecast_conditions>
      <forecast_conditions>
         <day_of_week data="Sat"/>
         <low data="43"/>
         <high data="68"/>
         <condition data="Clear"/>
      </forecast_conditions>
      <forecast_conditions>
         <day_of_week data="Sun"/>
         <low data="55"/>
         <high data="75"/>
         <condition data="Clear"/>
      </forecast_conditions>
   </weather>
</xml_api_reply>

Yahoo Weather API

The following URL will be used to access the weather data for Ottawa using the Yahoo Weather API (3369 is the WOEID for Ottawa):


The following is the result of performing the above query at time I was writing this article:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0"
    xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">
    <channel>
        <title>Yahoo! Weather - Ottawa, CA</title>
        <link>http://us.rd.yahoo.com/dailynews/rss/weather/Ottawa__CA/*http://weather.yahoo.com/forecast/CAXX0343_f.html</link>
        <description>Yahoo! Weather for Ottawa, CA</description>
        <language>en-us</language>
        <lastBuildDate>Thu, 08 Sep 2011 10:58 am EDT</lastBuildDate>
        <ttl>60</ttl>
        <yweather:location city="Ottawa" region="ON"
            country="Canada" />
        <yweather:units temperature="F" distance="mi" pressure="in"
            speed="mph" />
        <yweather:wind chill="66" direction="40" speed="12" />
        <yweather:atmosphere humidity="73" visibility=""
            pressure="30.14" rising="0" />
        <yweather:astronomy sunrise="6:31 am" sunset="7:25 pm" />
        <image>
            <title>Yahoo! Weather</title>
            <width>142</width>
            <height>18</height>
            <link>http://weather.yahoo.com</link>
            <url>http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif</url>
        </image>
        <item>
            <title>Conditions for Ottawa, CA at 10:58 am EDT</title>
            <geo:lat>45.42</geo:lat>
            <geo:long>-75.69</geo:long>
            <link>http://us.rd.yahoo.com/dailynews/rss/weather/Ottawa__CA/*http://weather.yahoo.com/forecast/CAXX0343_f.html</link>
            <pubDate>Thu, 08 Sep 2011 10:58 am EDT</pubDate>
            <yweather:condition text="Mostly Cloudy" code="28"
                temp="66" date="Thu, 08 Sep 2011 10:58 am EDT" />
            <description><![CDATA[
<img src="http://l.yimg.com/a/i/us/we/52/28.gif"/><br />
<b>Current Conditions:</b><br />
Mostly Cloudy, 66 F<BR />
<BR /><b>Forecast:</b><BR />
Thu - Partly Cloudy. High: 75 Low: 57<br />
Fri - Partly Cloudy. High: 79 Low: 53<br />
<br />
<a href="http://us.rd.yahoo.com/dailynews/rss/weather/Ottawa__CA/*http://weather.yahoo.com/forecast/CAXX0343_f.html">Full Forecast at Yahoo! Weather</a><BR/><BR/>
(provided by <a href="http://www.weather.com" >The Weather Channel</a>)<br/>
]]></description>
            <yweather:forecast day="Thu" date="8 Sep 2011" low="57"
                high="75" text="Partly Cloudy" code="30" />
            <yweather:forecast day="Fri" date="9 Sep 2011" low="53"
                high="79" text="Partly Cloudy" code="30" />
            <guid isPermaLink="false">CAXX0343_2011_09_09_7_00_EDT</guid>
        </item>
    </channel>
</rss><!-- api4.weather.sp2.yahoo.com uncompressed/chunked Thu Sep 8 08:32:54 
    PDT 2011 -->

Java Model - Mapped to Yahoo's XML Schema via XML Metadata

Since we can not supply a second set of mappings to an object model via annotations, we must supply subsequent mappings by leveraging MOXy's XML metadata.  By default MOXy's mapping document is used to supplement any annotations that are specified on the model.  However, if the xml-mapping-metadata-complete flag is set, then the XML metadata will completely replace the metadata provided by annotations (the annotations for the Google mapping will remain on the POJOs, but the xml-mapping-metadata-complete flag tells MOXy to ignore them).

<?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>


Demo

The following demo code will read the XML data for Yahoo's weather service, and marshal the objects back to XML.  Due to a MOXy bug regarding unmapped CDATA sections (https://bugs.eclipse.org/357145, this bug has been fixed in EclipseLink 2.3.1), a filtered XMLStreamReader was used to remove it from the XML input:

package blog.weather;

import java.util.HashMap;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.StreamFilter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;

import org.eclipse.persistence.jaxb.JAXBContextFactory;

public class YahooDemo {

    public static void main(String[] args) throws Exception {
        Map<String, Object> properties = new HashMap<String, Object>(1);
        properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, "blog/weather/yahoo-binding.xml");
        JAXBContext jc = JAXBContext.newInstance(new Class[] {WeatherReport.class}, properties);

        XMLInputFactory xif = XMLInputFactory.newFactory();
        StreamSource xml = new StreamSource("http://weather.yahooapis.com/forecastrss?w=3369");
        XMLStreamReader xsr = xif.createXMLStreamReader(xml);
        xsr = xif.createFilteredReader(xsr, new CDATAFilter());

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        WeatherReport weatherReport = (WeatherReport) unmarshaller.unmarshal(xsr);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(weatherReport, System.out);
    }

    private static class CDATAFilter implements StreamFilter {

        public boolean accept(XMLStreamReader xsr) {
            return XMLStreamReader.CDATA != xsr.getEventType();
        }

    }

}

Output

Below is the result of running the demo code.  The output represents the portion of the XML document that we had mapped to:

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0">
   <channel>
      <yweather:location city="Ottawa"/>
      <item>
         <yweather:forecast day="Thu" low="57" high="74" text="Partly Cloudy"/>
         <yweather:forecast day="Fri" low="53" high="79" text="Partly Cloudy"/>
      </item>
   </channel>
</rss>

Further Reading

If you enjoyed this post, then you may also be interested in:

4 comments:

  1. Hello,
    I have a quite big object model, already annotated. I need to be able to work with multiple versions of the same schema. There aren't many significant differences between schema versions, but each of these schemas is in a separate namespace. Do I have to create for each schema version complete bindings which will totally override the annotations? Or maybe there is a way to override just the mappings that change between versions? Is there a way to automatically generate bindings from a an annotated object model?

    ReplyDelete
    Replies
    1. If you do not specify xml-mapping-metadata-complete="true" on the xml-bindings element, then the metadata supplied via the mapping document will be used to augment rather than replace the existing metadata. You could leverage this to just tweak the namespace.

      Delete
  2. Hi,

    I am trying to implement the same scenario in my project. This worked fine when I had a small project and was able to map the bindings. But when I moved this logic to my existing mavenized web project, I keep getting the error

    java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.util.MethodInvoker.invoke(MethodInvoker.java:273)
    at com.tagcmd.api.core.BaseTest.executeMethod(BaseTest.java:30)
    at com.tagcmd.api.mrm.task.dao.impl.TaskDaoTest.task_rag_xml_to_proc_should_be_valid(TaskDaoTest.java:209)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
    Caused by: javax.xml.bind.JAXBException: property "eclipselink-oxm-xml" is not supported
    at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:130)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:202)
    at javax.xml.bind.ContextFinder.find(ContextFinder.java:363)
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:574)
    at com.tagcmd.api.mrm.task.dao.impl.TaskDao.convertTaskStatisticsToXMLString(TaskDao.java:723)
    ... 31 more

    Though I have jaxb.properties in the same package as my domain object. I have also placed the file in the package where the TaskDao.java is present.

    My code:

    TaskDao.java

    ....
    Map properties = new HashMap(1);
    properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, "src/main/java/com/tagcmd/domain/MyTask.xml");
    JAXBContext jc = JAXBContext.newInstance(new Class[] { Task.class }, properties);

    Marshaller m = jc.createMarshaller();
    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

    ReplyDelete
    Replies
    1. You need to make sure that you put the jaxb.properties file under the resources and not the java directory. Here is a link to an example:
      - https://github.com/bdoughan/blog20110322/tree/master/src/main/resources/blog/predicate

      Delete