I recently came across a very nice requirement where a REST service was required to produce a response both in JSON and XML format.
However, there is a bug with JDeveloper and the automatic generation of EJB/TopLink Entity annotations. This bug is reproduced when you choose to generate a REST service on that automatically generated EJB entity and you choose to produces both a JSON and an XML response.
What's even trickier is that this bug is only revealed at runtime when you choose to parse an XML response. It will throw an Internal Server Error (500) and if you inspect the log files you will see the following trace:
MessageBodyWriter not found for media type=application/xml, type=class java.util.ArrayList, genericType=java.util.List
So let's see in details how to reproduce this bug and how to fix it.
I will create a new application using the "Custom Application" template and will provide an application (bug-with-rest-service-app) and a project name (bug-with-rest-service).
What I will do is that I will create an EJB entity based on the "Countries" table from the HR sample database schema. So right-click your project, choose "New", "From Gallery" and under the "Business Tier" section, select the "EJB" category and then from the category items select "Entities from Tables".
Ensure "JPA 2.0 Entities (Java EE 6)" is selected and click next. Click "Next" in the "Persistence Unit" and "Types of Connection" steps and in the fourth step ("Database Connection"), click on the green plus button to create a new database connection (or choose an existing one from the drop down list).
In the fifth step select the "Countries" table and click "Next".
Specify an appropriate package name for creating the EJB entity and click "Next" and then "Finish".
You should have an offline database created with the "Countries" table, a persistence.xml file (JPA configuration file) generated under "META-INF" and your "Countries" EJB entity created under your defined java package path.
Next we need to create a facade and expose the methods that we want to expose on the "Counties" EJB entity. To do that we will use another JDeveloper wizard.
So right-click your project, choose "New" and from the context-menu select "Java Service Facade (JPA/TopLink). Specify a package for storing your facade and click "Next".
Leave all defaults on the second step and click "Next" and on the third step of the wizard click on "Finish".
So now we have a Facade which acts like our "controller", defining what is accessible on the "Countries" EJB entity and you should have also a test client which via the facade accesses one of the exposed operations (should be getCountriesFindAll).
If you run the test client you should get a list of countries printed in the log window.
We are now ready to create our REST service. Again for this we will use JDeveloper's REST build in capabilities to generate a REST service.
There are several ways to do that. We can very just right click our facade and create a REST service. However, this would result in a really bad design pattern. I highly recommend that you create a "wrapper" class on top of the facade and then expose that "wrapper" class as a REST service.
So right click your project, select "New" and select "Java Class". Give your Java class a name, specify a package and click "OK".
Create a method in your new class that just returns a list of counties using the facade class that you generated before.
We are now ready to create a RESTful service using the above class. To do so, right click your Java class and from the context menu select "Create RESTful Service". We only need to configure step 3 of the wizard.
You should have your "getAllCountries" method listed under the "Configure HTTP Methods" section.
In the "Type" field select the "GET" operation and click in the "Produces" field to open the "Media Types" popup and select the "application/json" and "application/xml" media types (so that our service is capable of returning either a response in JSON or XML format).
Click "Finish" to generate the REST service (ignore the return type warning).
If you inspect your Java class you should notice that it has been "decorated" with various annotations to instruct the runtime JVM engine to treat this class as a REST service.
We are now ready to test our REST service. To do so, right click your Java class and select from the context menu "Test Web Service". This should start your JDeveloper's embedded Weblogic Server and it should open an "HTTP Analyzer" to test your REST service.
If you click on the "Send Request" button, your REST service should be invoked and in the right response panel you should get a response listing all countries in JSON format.
If you recall a few steps before, we configured our REST service to be capable of returning a response back in both JSON and XML format. So why is JSON configured as the default response type?
The answer relies on the @Produces annotation in our Java class. The media type definition order is crucial. As we can see in our class, JSON is configured first and XML is configured as the alternative media type.
So how can we instruct a web service client to request a specific media type response? Well using the "Accept" HTTP header definition. So from the "HTTP Analyzer" window, click on the "HTTP Content" tab and in the "Accept" HTTP header property, update its value to just "application/xml".
If you invoke the service now, you should get an "Internal Server Error" exception (HTTP 500). And if you inspect the log window you should get the following stack trace:
MessageBodyWriter not found for media type=application/xml, type=class java.util.ArrayList, genericType=java.util.List<com.antonis.antoniou.rest.bug.entities.Countries
The reason for getting this exception is that because there is insufficient information in the "Countries" EJB class to help the JVM runtime to interpret the result and map it to the appropriate XML elements.
To fix this exception we need to decorate our "Countries" class with two additional annotations to instruct the engine to perform the proper Java to XML mapping. Therefore add the @XmlRootElement annotation just above the @Entity annotation to map the entire class to be the root XML element and just before each get method (getCountryId, getCountryName and getRegionId) add the @XmlElement annotation to map the countryId, countryName and regionId properties to XML elements.
Let's try to run our REST service again (stop the previous instance and run another test). From the "HTTP Analyzer" window, click on the "HTTP Content" tab, update the "Accept" HTTP Header property to "application/xml" and click on the "Send Request" button. You should now get a response back in XML format.
Update the "Accept" HTTP Header property to "application/json" and click on the "Send Request" button. You should get a response back in JSON format.
Download sample application: REST Service Producing JSON & XML Response Bug and Fix in JDeveloper 12.2.1
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment