3

Let's say, I have a REST styled controller mapping

@RequestMapping(value="users", produces = {MediaType.APPLICATION_JSON_VALUE})
public List<User> listUsers(@ReqestParams Integer offset, @ReqestParams Integer limit, @ReqestParams String query) {
    return service.loadUsers(query, offset, limit);
}

Serving JSON (or even XML) is not an issue, this is easy using ContentNegotation and MessageConverters

<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="favorPathExtension" value="true" />
    <property name="favorParameter" value="false" />
    <property name="ignoreAcceptHeader" value="false" />
    <property name="mediaTypes" >
        <value>
            html=text/html
            json=application/json
            xml=application/xml
        </value>
    </property>
</bean>

Now, I need to add support for PDF. Naturally, I want to use (Spring) MVC + REST as much as possible. Most examples I have found implement this with an explicit definition not using REST style, e.g.

@RequestMapping(value="users", produces = {"application/pdf"})
public ModelAndView listUsersAsPdf(@ReqestParams Integer offset, @ReqestParams Integer limit, @ReqestParams String query) {
    List<User> users = listUsers(offset, limit, query); // delegated
    return new ModelAndView("pdfView", users);
}

That works, but is not very comfortable because for every alternate output (PDF, Excel, ...) I would add a request mapping.

I have already added application/pdf to the content negotation resolver; unfortunately any request with a suffix .pdf or the Accept-Header application/pdf were be responded with 406.

What is the ideal setup for a REST/MVC style pattern to integrate alternate output like PDF?

knalli
  • 1,973
  • 19
  • 31
  • I might be misunderstanding, but do you want to serve the PDF file any time a request to this URL arrives with an Accept header that is compatible with PDF? I would think you only want to serve PDF files at certain URLs, and probably a distinct URL from a JSON/REST API. – matt b Mar 06 '14 at 21:03
  • Well basically, the idea was indeed using accept-header (and actually, the extension) for requesting a pdf. Why not? It would follow the content negotation pattern and the resource would exist only once. – knalli Mar 07 '14 at 09:02
  • I would think that adding application/pdf to the ContentNegotiationManagerFactoryBean is the right first step, but afterwards you need to then add a ViewResolver capable of generating PDF files. Did you do that as well? – matt b Mar 07 '14 at 13:54

3 Answers3

0

You can create a WEB-INF/spring/pdf-beans.xml like below.

 <bean id="listofusers" class="YourPDFBasedView"/>

And your controller method will return view name as listofusers.

@RequestMapping(value="users")
public ModelAndView listUsersAsPdf(@ReqestParams Integer offset, @ReqestParams Integer limit, @ReqestParams String query) {
    List<User> users = listUsers(offset, limit, query); // delegated
    return new ModelAndView("listofusers", users);
}

And you can use contentNegotiationViewResolver in this way:

 <bean class="org.springframework.web.servlet.view.XmlViewResolver">
            <property name="order" value="1"/>
            <property name="location" value="WEB-INF/spring/pdf-views.xml"/>
        </bean>

<!--
        View resolver that delegates to other view resolvers based on the content type
    -->
    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
       <!-- All configuration is now done by the manager - since Spring V3.2 -->
       <property name="contentNegotiationManager" ref="cnManager"/>
    </bean>

    <!--
        Setup a simple strategy:
           1. Only path extension is taken into account, Accept headers are ignored.
           2. Return HTML by default when not sure.
     -->
    <bean id="cnManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <property name="ignoreAcceptHeader" value="true"/>        
        <property name="defaultContentType" value="text/html" />
    </bean>

For JSON: Create a generic JSON view resolver like below and register it as bean in context file.

public class JsonViewResolver implements ViewResolver {
    /**
      * Get the view to use.
      *
      * @return Always returns an instance of {@link MappingJacksonJsonView}.
     */
    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        MappingJacksonJsonView view = new MappingJacksonJsonView();
        view.setPrettyPrint(true);      // Lay the JSON out to be nicely readable 
        return view;
    }
}

Same for XML:

public class MarshallingXmlViewResolver implements ViewResolver {

    private Marshaller marshaller;

    @Autowired
    public MarshallingXmlViewResolver(Marshaller marshaller) {
        this.marshaller = marshaller;
    }

    /**
     * Get the view to use.
     * 
     * @return Always returns an instance of {@link MappingJacksonJsonView}.
     */
    @Override
    public View resolveViewName(String viewName, Locale locale)
            throws Exception {
        MarshallingView view = new MarshallingView();
        view.setMarshaller(marshaller);
        return view;
    }
}

and register above xml view resolver in context file like this:

<oxm:jaxb2-marshaller id="marshaller" >
        <oxm:class-to-be-bound name="some.package.Account"/>
        <oxm:class-to-be-bound name="some.package.Customer"/>
        <oxm:class-to-be-bound name="some.package.Transaction"/>
    </oxm:jaxb2-marshaller>

    <!-- View resolver that returns an XML Marshalling view. -->
    <bean class="some.package.MarshallingXmlViewResolver" >
        <constructor-arg ref="marshaller"/>
    </bean>

You can find more information at this link: http://spring.io/blog/2013/06/03/content-negotiation-using-views/

Using all view resolver techniques, you can avoid writing duplicate methods in controller, such as one for xml/json, other for excel, other for pdf, another for doc, rss and all.

  • Hi, thank you for the answer, I'm waiting long time. Unfortunately, that is not the thing I'm looking for. With your solution, eiter I have still two controller bindings (one with @ResponseBody `List<> listUsers()` and one `MAV listUsersAsXY()`), or I would have only the MAV variant using the JSON ViewResolver but without using @ResponseBody actually (which I have to use). Do you go with me? – knalli Mar 06 '14 at 15:38
  • Why not return MAV in all cases, it will handle your @ResponseBody requirement also. I have one sample configuration here. http://punitusingh.wordpress.com/2014/03/07/spring-viewresolvers-can-have-same-bean-id-that-we-can-use-while-returning-viewname-from-controller-method/, basic idea to give ordering on the resolvers. – punitusingh Mar 07 '14 at 14:39
  • When you return a view, your json response will be taken care by org.springframework.web.servlet.view.json.MappingJacksonJsonView class, and xml response will be taken care by org.springframework.web.servlet.view.xml.MarshallingView class as shown in given link in my above comment. And for pdf you need to define your own view resolver, the same way I have defined pdfResolver bean. – punitusingh Mar 07 '14 at 14:45
0

Knalli, if you replace @ResponseBody with ModelAndView(), you can achieve both the features. Is there any reason you want to keep @ResponseBody ? I just want to know if I am missing anything, just want to learn.

Other option is to write HttpMessageConverters then: Some samples are here.

Custom HttpMessageConverter with @ResponseBody to do Json things

http://www.javacodegeeks.com/2013/07/spring-mvc-requestbody-and-responsebody-demystified.html

Community
  • 1
  • 1
  • I was trying to achieve this all with content negotation; and the converters do not work with MAV? When I return a MAV, I would have do define a view which is actually already to specific in the situation. – knalli Mar 07 '14 at 09:15
0

This is working sample. I have configured contentnegotiationviewresolver for this, and give highest order. After that I have ResourceBundleViewResolver for JSTL and Tiles View, then XmlViewResolver for excelResolver, pdfResolver, rtfResolver. excelResolver, pdfResolver, rtfResolver. XmlViewResolver and ResourceBundleViewResolver works only with MAV only, but MappingJacksonJsonView and MarshallingView takes care for both MAV and @ResponseBody return value.

<bean id="contentNegotiatingResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="order"
                  value="#{T(org.springframework.core.Ordered).HIGHEST_PRECEDENCE}" />
<property name="mediaTypes">
            <map>
                <entry key="json" value="application/json" />
                <entry key="xml" value="application/xml" />
                <entry key="pdf" value="application/pdf" />
                <entry key="xlsx" value="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
                <entry key="doc" value="application/msword" />
            </map>
        </property>

        <property name="defaultViews">
            <list>
                <!-- JSON View -->
                <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />

                <!-- XML View -->
                <bean class="org.springframework.web.servlet.view.xml.MarshallingView">
                        <constructor-arg>
                            <bean id="jaxbMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
                            <property name="classesToBeBound">
                            <list>
                                <value>Employee</value>
                                <value>EmployeeList</value>
                            </list>
                            </property>
                            </bean>
                        </constructor-arg>
            </bean>
            </list>
        </property>
        <property name="ignoreAcceptHeader" value="true" />
 </bean>
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver"
        id="resourceBundleResolver">
        <property name="order" value="#{contentNegotiatingResolver.order+1}" />
    </bean>
<bean id="excelResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
       <property name="location">
           <value>/WEB-INF/tiles/spring-excel-views.xml</value>
       </property>
       <property name="order" value="#{resourceBundleResolver.order+1}" />
    </bean>

    <bean id="pdfResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
       <property name="location">
           <value>/WEB-INF/tiles/spring-pdf-views.xml</value>
       </property>
       <property name="order" value="#{excelResolver.order+1}" />
    </bean>

    <bean id="rtfResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
       <property name="location">
           <value>/WEB-INF/tiles/spring-rtf-views.xml</value>
       </property>
       <property name="order" value="#{excelResolver.order+1}" />
    </bean>

And our XMLViewResolver spring-pdf-views.xml looks like this.

<bean id="employees"
        class="EmployeePDFView"/>

And EmployeePDFView will have code for generating pdf and writing pdf byte stream on Response object. This will resolve to rest url that will end with .pdf extension, and when you return MAV with "employees" id.