Using JSON for communication between GWT and Django

Here is the final entry in my blog series about merging GWT and Django. It gives an example of how you can use JSON and JSNI for the communication between your GWT client and Django server.

The example is from a Django/GWT social network webpage I constructed called StartMeUp. It uses a GET request in GWT to fetch data about the selected member of the website, including blog posts on the site. The data is retrieved from the database in the Django view “getMemberData(request)” where it is encoded into JSON format using simplejson. After that, the JSON data is returned to the GWT client where it is interpreted by the class “JsonData” using JSNI.

The best thing to look at first is the function “private void serverGetMemberData()” in the “MemberPage” class further down on this page. This is the function that fetches data about the members of the website from Django to GWT. In it, all other relevant functions are called.

At the bottom of the page you will find more tips and tricks on how to implement the GWT-Django communication.

Note 1: In order to use the built in JSON parsing ability of GWT you have to add <inherits name=”com.google.gwt.json.JSON”/> to the GWT_PROJECT_NAME.gwt.xml file found in the folder GWT_PROJECT_FOLDER\src\GWT_PROJECT_NAME of your GWT project. For more information about JSON and JSNI in GWT, see the GWT tutorial here: https://developers.google.com/web-toolkit/doc/latest/tutorial/JSON.

Note 2: The formatting of the source code looks a bit strange due to the bad line breaks inserted by the web. If you want a better looking version, just copy the code from the blog and paste it into a text file. You should then get my original code layout.

In GWT:

Class JsonData

class JsonData extends JavaScriptObject {
  // Overlay types always have protected, zero argument constructors.
  protected JsonData() {}

  // JSNI methods.
  public final native int getPrimaryKey() /*-{ return this.pk; }-*/;
  public final native int getID() /*-{ return this.id; }-*/;
  public final native String getUsertype() /*-{ return this.user_type; }-*/;
  public final native String getUsername() /*-{ return this.username; }-*/;
  public final native String getName() /*-{ return this.name; }-*/;
  public final native String getLocation() /*-{ return this.location; }-*/;
  public final native String getEmail() /*-{ return this.email; }-*/;
  public final native String getBranch() /*-{ return this.branch; }-*/;
  public final native String getProduct() /*-{ return this.product; }-*/;
  public final native String getBlogHeadline() /*-{ return this.blog_headline; }-*/;
  public final native String getBlogTextArea() /*-{ return this.blog_text_area; }-*/;

  // Non-JSNI methods.
//  public final String total() {
//    return getUsername() + getID();
//  }
}

Class Communicator

/**
 * Utility class for communication with the server (for POST and GET requests).
 * 
 * @author Anders Löfgren
 *
 */
public class Communicator 
{

	 public static final native JsArray asArrayJsonData(String json) /*-{
	    return JSON.parse(json);
	  }-*/;

	/**
	 * String sent to the server must only consist of numbers, letters, !, ?, @, spaces, dots or commas.
	 * This prevents MySQL code infusion. Note that there may be other unsafe characters. Check this if used in production code!
	 * 
	 * @param input
	 * @return True if the input only consist of numbers, letters, !, ?, @, spaces, dots or commas. False otherwise.
	 */
	public static boolean safeServerInput(String input)
	{
		if (!input.matches("^[0-9a-zA-Z\t\n\r !\\?@\\.,]*$")) 
		{ 
			Window.alert("Error! Only input made up of numbers, letters, !, ?, @, spaces, dots or commas are allowed.");
			return false; 
		}
		else
			return true;
	}

	/** Makes a GET-request to the server at the url defined by url_ending and then processes the response according to the request_callback.
	 * @return Void. Response is handled in request_callback.
	 */
	public static void doGET(String url_ending, String GET_data, RequestCallback request_callback)
	{
		// Django will automatically redirect this URL to "http://127.0.0.1:8000/url_ending/" if no exact URL match is found, unless "APPEND_SLASH = false" is added to the Django settings file.
		final String url = GWT.getHostPageBaseURL() + url_ending; // "http://127.0.0.1:8000/url_ending";
		RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, URL.encode(url));

		try
		{
			builder.setHeader("charset", "utf-8");

			Request request = builder.sendRequest(GET_data, request_callback);
		}
		catch (RequestException e)
		{
			// Couldn't connect to server
			Window.alert("Error: Couldn't connect to: " + url +" \nError message: " + e.getMessage());
		}
	}

	/** Makes a POST-request to the server at the url defined by url_ending with the data in POST_data and then processes the response according to the request_callback.
	 * @return Void. Response is handled in request_callback.
	 */
	public static void doPOST(String url_ending, String POST_data, RequestCallback request_callback)
	{
		// NOTE! The URL to login has to end with a slash if the request uses POST with Django! 
		// (otherwise no exact URL match will be found in the Django URLs file => redirect => loss of POST data)
		final String url = GWT.getHostPageBaseURL() + url_ending; // "http://127.0.0.1:8000/url_ending";
		RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, URL.encode(url));

		try
		{
			// X-CSRFToken header must be set in order for Django to accept a non secure HTTP request (that is everything except for GET, HEAD, OPTIONS or TRACE). 
			// This prevents Cross Site Request Forgery. The cookie csrftoken is generated and sent from the django.middleware.csrf.CsrfViewMiddleware in the Django server.
			builder.setHeader("X-CSRFToken", Cookies.getCookie("csrftoken"));
			builder.setHeader("charset", "utf-8");

			Request request = builder.sendRequest(POST_data, request_callback);
		}
		catch (RequestException e)
		{
			// Couldn't connect to server
			Window.alert("Error: Couldn't connect to: " + url +" \nError message: " + e.getMessage());
		}
	}
}

Class MemberPage

public class MemberPage
{
	private TextBox name_text_box = new TextBox();
	private TextBox location_text_box = new TextBox();
	private TextBox email_text_box = new TextBox();

	private void updateAllTextBoxValues(JsonData server_values)
	{
		name_text_box.setText(server_values.getName());
		location_text_box.setText(server_values.getLocation());
		email_text_box.setText(server_values.getEmail());
	}

	private void serverGetMemberData()
	{
		final String url_ending = "getMemberData/";

		RequestCallback request_callback = new RequestCallback()
		{
			public void onError(Request request, Throwable exception)
			{
				// Couldn't connect to server (could be timeout, SOP violation, etc.)
				Window.alert("Error: Couldn't connect to: " + url_ending +" \nError message: " + exception.getMessage());
			}

			public void onResponseReceived(Request request, Response response)
			{
				if (200 == response.getStatusCode())
				{
					// Process the response in response.getText()
					String text_response = response.getText();

					if(text_response == null || text_response.isEmpty())
					{
						return;
					}
					else if(text_response.equals("No user blog entry exist."))
					{
						return;
					}
					else
					{
						JsArray blog_post_array = Communicator.asArrayJsonData(text_response);

						if(blog_post_array != null)
						{
							updateAllTextBoxValues(blog_post_array.get(0)); // Same name, location and email for all posts, since they are from the same member.

							for(int element_number = 0; element_number < blog_post_array.length(); element_number++)
							{
								/* The appendToBlogPosts function displays all the data found in the blog_post_array in blog posts on the client side.
								* The function is not included in this example because it is somewhat large and likely complicates understanding of the code.
								*/
								// appendToBlogPosts(blog_post_array.get(element_number)); 
							}
						}
					}
				}
				else
				{
					Window.alert("Error: Couldn't connect to: " + url_ending +" \nError message: " + response.getStatusText());
				}
			}
		};
		Communicator.doGET(url_ending, "", request_callback);
	}
}

In Django:

urls.py

urlpatterns = patterns('',
    (r'^getMemberData/$','StartMeUpApp.views.getMemberData'),
)

views.py in StartMeUpApp

def getMemberData(request):
    current_user = request.user
    user_blogposts = BloggPosts.objects.filter(owner=current_user)
    result_list = []

    if(user_blogposts): # If user_blogposts exists.
        user_blogposts = user_blogposts.order_by("-pub_date")
        # user_blogpost = user_blogposts[:10] # Get the ten last blogposts. Not used, but another example of what you can do.

        for blogpost in user_blogposts:
            result_list.append(dict(id=blogpost.id, blog_headline=blogpost.headline, blog_text_area=blogpost.content, 
                               name=current_user.get_profile().name, location=current_user.get_profile().location, email=current_user.email))
    else:
        return HttpResponse("No user blog entry exist.", mimetype="text/plain")

    return HttpResponse(simplejson.dumps(result_list, ensure_ascii=False, indent = 0), mimetype="application/json")


Tips and Tricks

>   Because an exact match to the URL in the Django URLs file is required, for me, all HTTP request URL:s of POST type in GWT must end with “/”, e.g. http://127.0.0.1:8000/login/ and not http://127.0.0.1:8000/login. Otherwise Django cannot handle them, and an internal server error will be generated (ERROR 500).

>   It may sometimes help to add the @ensure_csrf_cookie decorator to the Django index view, in order to make sure that POST requests work.

>   For the POST data, I use the following structure: “Variable name”=”Value”. Variables are separated by an “&”. E.g.: username=zezame&password=letmein. Then on the Django server side you can use (if you send a POST request): request.POST[‘username’] to get the value of the username variable (“zezame” in the example).

>   I’ve heard about two frameworks that may be helpful when setting up the GWT-Django communication, but I haven’t actually tried them myself. Still, in case you want to give them a go they are:

RestyGWT on the GWT side to transfer data (handle GET/POST requests) and encode/decode data to/from JSON in GWT (see “REST API” headline): http://restygwt.fusesource.org/documentation/restygwt-user-guide.html and http://stackoverflow.com/questions/5658936/gwt-client-how-to-convert-object-to-json-and-send-to-server

Piston on the Django side: https://bitbucket.org/jespern/django-piston/wiki/Home

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: