Monday, 9 November 2009

File uploading with smartgwt on Google App Engine

On this post, I'll tackle two subjects, the server-side validation of a form, and the handling of the file upload operation. The file upload operation is probably of most interest since many ask themselves how can they persist files to the Google App Engine.
Like in the previous post, I am using json for serializing the server response to the client.

Server-side validation 

Server side validation in smartgwt is easy. In case that I detect a validation error, I just have to add an error object to the responsse message in which each field is matched by an attribute. Our AgendaEntry class has the attributes firstName,lastName, meaning  the error message should look like this:


 {
               errors:   {
                                 firstName: "Cannot assign admin",
                                 lastName: "Cannot assign admin"
                             }
 }



But to output this JSON message we need a validation mechanism on the server. For the sake of coming up with an example, I will define a validator that will exclude any entries that bear the values "admin","admin" for firstName and lastName :


package javagoogleappspot.examples.controller;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import javagoogleappspot.examples.model.AgendaEntry;

public class AgendaValidator implements Validator {

    public boolean supports(Class clazz) {
        // TODO Auto-generated method stub
        return AgendaEntry.class.isAssignableFrom(clazz);
    }

    public void validate(Object object, Errors errros) {
        AgendaEntry agendaEntry = (AgendaEntry) object;
        if (("admin".equals(agendaEntry.getFirstName()))&&
                ("admin".equals(agendaEntry.getLastName())))
                {
                    errros.rejectValue("firstName", "Cannot assign admin");
                    errros.rejectValue("lastName", "Cannot assign admin");                    
            }
    }

}


Since we use a MultiActionController, I cannot wire a validator instance. I will define a method that will call our validator :


    protected BindException bindObject(HttpServletRequest request,
            Object command, Validator validator) throws Exception {
        BindException errors = new BindException(command,
                getCommandName(command));
        if (validator.supports(command.getClass())) {
            ValidationUtils.invokeValidator(validator, command, errors);
        }

        return errors;
    } 

    
    public ModelAndView create(HttpServletRequest req,
            HttpServletResponse resp,AgendaEntry agendaEntry) throws Exception {
        BindException errors = bindObject(req, agendaEntry, new AgendaValidator());
        Map model = new HashMap<string, object="">();            
        ModelAndView modelAndView = new ModelAndView();

        if (!errors.hasErrors()){
            List<object> objects = new ArrayList<object>();                     
            agendaEntryDAO.create(agendaEntry);             
            objects.add(agendaEntry);                                     
            model.put("jsonResponse", objects);                 
        }         
          else
        {             
            model.put("jsonErrorResponse", errors);
        }         
 
        modelAndView.addAllObjects(model);                             
        View jsonView = new JSONView();         
        modelAndView.setView(jsonView);         
        return modelAndView;             
       } 



As you can observe I am relying on the Spring controller to map request parameteres passed from the client form to my Agenda bean.

Our Spring view bean, will take care of serializing the error (javagoogleappspot.examples.util.JSONView).
The view has to be provided with either a "jsonResponse" mapping if everything goes well or a "jsonErrorResponse" mapping in case we  detect an error:


package javagoogleappspot.examples.util;

import java.io.OutputStream;
import java.util.Date;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.servlet.View;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class JSONView implements View {

    public String getContentType() {
          return "application/x-javascript";
    }

    public void render(Map map, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new DateTypeAdapter()).create();
        boolean hasErrors = false;
        String jsonResponse = "[]";
        Object model = null;

        OutputStream outputStream = response.getOutputStream();
        response.setContentType("application/x-javascript");                      
        if (map!=null){
            model = map.get("jsonResponse");
            if (model==null){
                model = map.get("jsonErrorResponse");
                hasErrors = true;    
            }
        }
        
        if (model==null)
            jsonResponse = "[]";
        else{
            if (hasErrors){
                BindException errors = (BindException)model;
                StringBuffer sb = new StringBuffer();
                sb.append("{");            
                int i = 0;
                for (Object error:errors.getFieldErrors()){
                    if (i>0)
                        sb.append(",");                        
                    FieldError fieldError = (FieldError) error;
                    sb.append(fieldError.getField());
                    sb.append(": \"");
                    sb.append(fieldError.getCode());                
                    sb.append("\"");                                    
                    i++;
                }                
                sb.append("}");                
                jsonResponse = "{errors:"+sb.toString()+"}";                                    
                }
            else
                jsonResponse = gson.toJson(model);                                    

        }
        
        outputStream.write(jsonResponse.getBytes());

        
    }

}




Make sure you empty the byte array retrieved from the datastore, you will avoid the overhead of writing these objects on the wire. I did it by using a serializing adapter and registered it with my Gson instance :


package javagoogleappspot.examples.util;

import java.lang.reflect.Type;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class ByteArrayTypeAdapter implements JsonSerializer,
        JsonDeserializer {

    public JsonElement serialize(byte[] src, Type typeOfSrc,
            JsonSerializationContext context) {
        return new JsonPrimitive("");
    }

    public byte[] deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException {
            return new byte[0];
    }
}






On client side, the return callback must associate the returning error object with the form. Smartgwt will apply the errors to the appropiate fields.


  form.fetchData(form.getValuesAsCriteria(),new DSCallback() {
              public void execute(DSResponse response, Object rawData,DSRequest request) {
                            JavaScriptObject data = response.getAttributeAsJavaScriptObject("data");
                            if (data!=null){
                                JavaScriptObject object = (JavaScriptObject) JSOHelper.getObjectArrayValue(data, 0);                            
                                JavaScriptObject errors = (JavaScriptObject) JSOHelper.getAttributeAsObject(object, "errors");                
                                Map errorsMap = JSOHelper.getAttributeAsMap(object, "errors");
                                
                                if (errors!=null){
                                      response.setStatus(RPCResponse.STATUS_VALIDATION_ERROR);   
                                      response.setErrors(errors);  
                                      form.setErrors(errorsMap, true);
                                    }
                                else
                                {
                                    int id = JSOHelper.getAttributeAsInt(object,"id");
                                    form.setValue("id", id);                    
                                    uploadFiles(response,form);                                            
                                    getWinModal().destroy();
                                    getGrid().invalidateCache();
                                    getGrid().fetchData(getFilter().getSearchForm().getSearchForm().getValuesAsCriteria());
                                    
                                }                        
                            }
                        }
    
                    });    





Client side validation 

to validate your form the following will do:



                boolean valid = form.validate();
                if (valid) {
                            
                          /**
                         Your block of code
                         **/ 
                } 



File upload

To persist a file attachement I followed Google's guidelines for persisting binary data. Please review Google app engine - Defining Data Classes :

As for the libraries needed, make sure you have commons-fileupload-1.2.1.jar both in runtime and on your build pat.

Now that we have the upload form ready, let's specify the target frame. The posting our request, we do not want to replace the current page, so we'll add a hidden frame to the main page - Agenda.html :


    <iframe height="0" width="0" id="upload_frame" name="upload_frame" src="" >
    </iframe> 

 
Our file attachement requires to add a multipart reference on our model (file attribute), it will encapsulate the multipart item sent by the cilent's browser.

Upload error handling

Initially I tried using  rest datasource  for submitting this form, but it does not work with multipart fields.

My concern was that the controller must return some kind of a message to the client, otherwise when an error fires, the client will continue his work unaware that the upload operation actually failed. How should I do that? Smartgwt does not define any callback for multipart form submit. I found a workaround. As the output of the controller stream goes into the upload_frame,  what I will do is to convince the controller to output a javascript code that will a run on the client and call a GWT native javascript (JNSI) method.

To handle a MultipartException I will add a HandlerExceptionResolver implementation to our spring context :


package javagoogleappspot.examples.util.multipart;

import javagoogleappspot.examples.util.UploadErrorView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;

public class MultipartExceptionHandler implements HandlerExceptionResolver {
    private static final Log log = LogFactory
            .getLog(MultipartExceptionHandler.class);

    public ModelAndView resolveException(HttpServletRequest request,
            HttpServletResponse response, Object object, Exception exception) {
        ModelAndView modelAndView = null;


        if (exception instanceof MultipartException) {
            try {
                modelAndView =     new ModelAndView();
                View uploadErrorView = new UploadErrorView();
                modelAndView.setView(uploadErrorView);
            } catch (Exception e) {
                log.error(e);
            }
        }

        return modelAndView;
    }

}







The handler redirects to a special view that will write the function callback to call the client :


package javagoogleappspot.examples.util;

import java.io.OutputStream;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.View;

public class UploadErrorView implements View {

    public String getContentType() {
          return "text/html; charset=UTF-8";
    }

    public void render(Map map, HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        OutputStream outputStream = response.getOutputStream();
        response.setContentType("text/html; charset=UTF-8");
        outputStream.write("".getBytes());               
        
    }

}



On the client side, I will add a GWT javascript native method :


// Set up the JS-callable signature as a global JS function.
 private native void addJSCallbacks() /*-{
   $wnd.showErrorMsg = 
     @javagoogleappspot.examples.web.client.forms.AgendaEntryDetail::showErrorMsg();
 }-*/;




Download the application source code here

I have deployed a working example to App Engine here

About Me

My Photo