Smartgwt RestDataSource with SpringMVC and cross-c

2019-03-04 12:53发布

问题:

After a lot of work, I have an existing back-end web services application that is powered by Spring-RS, Spring MVC, Spring controllers, and these controllers user Jackson within the Spring framework to convert responses to JSON.

Here is part of the WEB-INF/myproject-servlet.xml

<mvc:annotation-driven>
    <mvc:message-converters register-defaults="true">
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
          <property name="objectMapper">
             <bean class="com.fasterxml.jackson.databind.ObjectMapper">

                     <property name="dateFormat">
                        <bean class="java.text.SimpleDateFormat">
                            <constructor-arg type="java.lang.String" value="yyyy-MM-dd"></constructor-arg>
                        </bean>
                     </property>
             </bean>
          </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

<bean id="jsonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
    <property name="supportedMediaTypes" value="application/json"/>
</bean>

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="messageConverters">
      <list>
        <ref bean="jsonHttpMessageConverter" />
      </list>
  </property>
</bean>

This web-services app works great! I can deploy the WAR to my local tomcat, and it deploys fine. I can unit test he controller to make sure the URL is correct and the web-app is correct configured within Spring. I can hit the URL and get JSON data back exactly as I expected it to. The url is:

http://mylocalhost/myproject/invoices/invoiceId/1

returns 1 invoice.

Now, I am running a SmartGWT web-app, the free version, and I have a RestDataScource controller. I have written many SmartGWT web-apps before, and these apps were all inclusive: entities, dao's, service layer, controllers, and datasources. With this, there was no cross-client issues at all provided the controllers and the datasources were within the same app. And I am not against doing that again, but I want to try to separate them apart.

I just recently saw that this does not work!!! With my SmartGWT web-app running locally within Jetty for development mode. The starting URL is:

     http://mylocalhost:8888/myapp

And when this tries to call the back-end on

    http://mylocalhost:8080/my-ws, then my listgrid gives me a warning message.

If I can just add the one line: RPCManager.setAllowCrossDomainCalls(true); Do I add this within my RESTDataSource? Where do I add this to? And will it really just make everything work? Is there anything else I need to add?

So, I was looking at XJSONDataSource and I saw that I needed to make a few changes to my RestDataSource to convert it to a XJsonDataSource. There is some great information here with another posting and they suggested adding:

   // Where do I put this line?   the controller or the datasource
   String callbackString = request.getParameter("callback");

   // Where do I put this code?  the controller or the datasource
   response.setContentType("text/X-JSON");
   response.getWriter().write( callbackString + " ( " + JSONstring + " ) " );
   response.setStatus(HttpServletResponse.SC_OK);  

I am not sure where this code goes, so I need some extra help there. As far as the controller goes, here is part of what it looks like:

    @RequestMapping(value = "/invoiceId", method = RequestMethod.GET, headers = "Accept=application/json")
    public @ResponseBody
        InvoiceDetailDTO getContactTypeByUserId(@RequestBody String invoiceNumber)
     {
         InvoiceDetailDTO invoiceDetailDto = invoiceService.getInvoiceDetail(invoiceNumber);

        // invoiceDetailDto is automatically converted to JSON by Spring
        return invoiceDetailDto;
     }

In the code above with "request" and "response" have to go into the controller, how do I go about that?

Ultimately, I'd love to just use my RestDataSource and tweak it to work the way I want it to, and ignore any of these cross-site issues. If I do need to use XJSONDataSource, I just need some real good examples, and an example on how to tweak my controllers if needed.

Thanks!

回答1:

RPCManager.setAllowCrossDomainCalls(true); should be called during early stages of initialization (e.g.- onModuleLoad()).

getContactTypeByUserId might have to add Access-Control-Allow-Origin as a response header with proper value.
Check http://en.wikipedia.org/wiki/Cross-origin_resource_sharing.
Based on http://forums.smartclient.com/showthread.php?t=15487, SmartGWT should handle cross domain requests on its own.

As a worst case scenario, you might have to send a JSONP style response along with required headers to get this working.
In that case, its probably best to have a separate method, similar to following, to serve SmartGWT requests.
I haven't worked with XJSONDataSource, so following is only a guideline.

// use a slightly different URI to distinguish from other controller method
@RequestMapping(value = "/invoiceId/sgwt", method = RequestMethod.GET, headers = "Accept=application/json")
public @ResponseBody String getContactTypeByUserIdForSgwt(@RequestBody String invoiceNumber,
        HttpServletRequest request, HttpServletResponse response) {

     // can reuse normal controller method
     InvoiceDetailDTO invoiceDetailDto = getContactTypeByUserId(invoiceNumber);

     // use jackson or other tool to convert invoiceDetailDto to a JSON string
     String JSONstring = convertToJson(invoiceDetailDto);

    // will have to check SmartGWT request to make sure actual parameter name that send the callback name
    String callbackString = request.getParameter("callback"); 

    response.setContentType("text/X-JSON");

    return  callbackString + " ( " + JSONstring + " ) " ;
 }

Update

Probably a good idea to clean up code (or start from scratch/minimum) due to left overs from previous efforts.

There are three phases in solving this:
1. Get SmartGWT to work correctly, without using the service
2. Get the service to work correctly with CORS requests
3. Switch SmartGWT to use the service

Phase 1 should be used to iron out any client side issues.
Skip to phase 2, if client is working with the service when deployed in the same host/domain.

Phase 1
For this, its possible to use a data URL that provide a static response, as explained in RestDataSource JSON responses.
Place the sample response in a file similar to test.json and make it accessible from client web application.
Keep the DataSource code to a minimum and use setDataURL(); with test.json location.

test.json - change (and add if needed) field names and values

{    
 response:{
    status:0,
    startRow:0,
    endRow:3,
    totalRows:3,
    data:[
        {field1:"value", field2:"value"},
        {field1:"value", field2:"value"},
        {field1:"value", field2:"value"},
    ]
 }
}

DataSource

public class TestDS extends RestDataSource {

    private static TestDS instance = new TestDS();

    public static TestDS getInstance() {
        return instance;
    }

    private TestDS() {
        setDataURL("data/test.json");       // => http://<client-app-host:port>/<context>/data/test.json
        setDataFormat(DSDataFormat.JSON);
        // setClientOnly(true);

        DataSourceTextField field1 = new DataSourceTextField("field1", "Field 1");
        DataSourceTextField field2 = new DataSourceTextField("field2", "Field 2");

        setFields(field1, field2);
    }
}

Phase 2
Check references for additional details.

Headers of a failed preflight CORS request made from a page hosted in localhost:8118, and service hosted in localhost:7117.
Failed due to different port. Will fail on different scheme (https/ftp/file/etc.) or different host/domain as well.

Host: localhost:7117
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://localhost:8118                   <= indicates origin to which access should be granted
Access-Control-Request-Method: GET              <= indicates the method that will be used in actual request
Access-Control-Request-Headers: content-type    <= indicates the headers that will be used in actual request

Server: Apache-Coyote/1.1
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS
Content-Length: 0

Request/response header pairs of a successful request.

Host: localhost:7117
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://localhost:8118
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type

Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: http://localhost:8118
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Content-Type
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS
Content-Length: 0

Host: localhost:7117
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Referer: http://localhost:8118/cors-test.html
Origin: http://localhost:8118

Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Content-Type: application/json
Transfer-Encoding: chunked

In order to support CORS requests, service backend must respond correctly to the preflight OPTIONS request, not just the service call.
This can be done using a ServletFilter.

<filter>
    <filter-name>corsfilter</filter-name>
    <filter-class>test.CorsFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>corsfilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

public class CorsFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equals(request.getMethod())) {
            response.addHeader("Access-Control-Allow-Origin", "http://localhost:8118");

            // list of allowed methods, Access-Control-Request-Method must be a subset of this
            response.addHeader("Access-Control-Allow-Methods", "GET");
            // list of allowed headers, Access-Control-Request-Headers must be a subset of this
            response.addHeader("Access-Control-Allow-Headers", "Content-Type, If-Modified-Since");

            // pre-flight request cache timeout
            // response.addHeader("Access-Control-Max-Age", "60");
        }
        filterChain.doFilter(request, response);
    }
}

@RequestMapping(method = RequestMethod.GET, value = "/values", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map> getValues() {
    List<Map<String, Object>> values = getValues(); // handle actual data processing and return a list suitable for response

    SgwtResponse sgwtResponse = new SgwtResponse(); // A POJO with basic (public) attributes
    sgwtResponse.status = 0L;
    sgwtResponse.startRow = 0L;
    sgwtResponse.endRow = Long.valueOf(values.size());
    sgwtResponse.totalRows = sgwtResponse.startRow + sgwtResponse.endRow;
    sgwtResponse.data = values; // java.util.List

    Map<String, SgwtResponse> jsonData = new HashMap<String, SgwtResponse>();
    jsonData.put("response", sgwtResponse);

    HttpHeaders headers = new HttpHeaders();
    headers.add("Access-Control-Allow-Origin", "*"); // required

    return new ResponseEntity<Map>(jsonData, headers, HttpStatus.OK);
}

A simple test page that use jQuery to retrieve a JSON response using XHR.
Change URL and deploy in client web application to directly test service, without using SmartGWT.

<!DOCTYPE html>
<html>
    <head>
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        <script>
            $(document).ready(function () {
                $("#retrieve").click(function () {
                    $.ajax({
                        type: "GET",
                        contentType: "application/json",
                        url: "<URL-of-service>",
                        dataType: "json",
                        success: function (data, status, xhr) {
                            $("#content").text(JSON.stringify(data, null, 2));
                        },
                        error: function (xhr, status, error) {
                            $("#content").text("Unable to retrieve data");
                        }
                    });
                });
            });
        </script>
    </head>
    <body>
        <input type="button" id="retrieve" value="Retrieve"/>
        <div id="content"/>
    </body>
</html>

If-Modified-Since header was required in Access-Control-Allow-Headers for SmartGWT.
Use RPCManager.setAllowCrossDomainCalls(true); during SmartGWT initialization to avoid the warning.

Most modern browsers (Browser compatibility1) and SmartGWT RestDataSource support CORS requests.
Use XJSONDataSource only when you have to rely on JSONP, due to browser incompatibility with CORS requests.

Sending Access-Control-Allow-Origin: * for pre-flight request will allow any site to make cross domain calls to the service, which could pose a security issue, plus * can not be used in certain CORS requests.
Better approach is to specify the exact site to which cross domain requests are allowed - Access-Control-Allow-Origin: http://www.foo.com.
Probably not required in this case, but check Access-Control-Allow-Origin Multiple Origin Domains? if needed, to find ways to allow multiple sites to make CORS requests.

References:
[1] https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS
[2] http://java-success.blogspot.com/2012/11/cors-and-jquery-with-spring-mvc-restful.html