GWT Chrome extensions, with a connection to a server of their own.
While this demonstrates specifically how to enable to communication from a Chrome extension, there is
no difference in code if you wish to deploy your client code else where (not on Chrome).
Many web applications choose to deploy their Server code & Client code on the same application server, like JBoss or Tomcat.
As you know in GWT there are several ways to communicate with a server.
One of them is RPC for instance, which gives you the ability to invoke methods on a Java interface,
not having to deal with anything but Java code. Which is cool, as it makes a very smooth transition from
the client side code to the server side code.
However, when we want to implement this in a Chrome extension things get a bit trickier.
The reason is that you'd have to separate your server module from your Chrome extension module
(as we can't deploy Server side classes to Chrome - it runs only JavaScript),
and would need a shared model between the two in order to communicate.
It's not hard to do at all, it's only a longer example to make.
(In Eclipse, if you create a new "Web Application Project" using Google's plugin, you will have the client & server code in the same project, with a "shared" directory which serves a model bridge between the two).
Another option that exists, is using JSONP communication to the server.
In our case - this makes it a lot simpler.
We will expose some REST API on a Server we'll implement, for getting images, and send a JSONP request from the QuickPik extension to retrieve those images. Obviously, it can be anything else you want it to be in your application.
TO THE CODE!!!
The Server
In order to create a REST service and expose it, we will use the Jersey library which is an implementation for
building RESTful web services. It's very simple.
The following QuickpikService class, is our REST service:
package quickpik.server.web;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import com.sun.jersey.api.json.JSONWithPadding;
@Path("/quickpik")
public class QuickPikService {
private final static int BUFFER_SIZE_IN_BYTES = 1024;
private final static String FLICKR_API = "http://api.flickr.com/services/feeds/photos_public.gne?format=json&tagmode=all&tags=" ;
@GET
@Path("searchPhotos")
@Produces({ "application/x-javascript", MediaType.APPLICATION_JSON})
public JSONWithPadding getPhotos(@QueryParam("searchExp") String searchExp,
@QueryParam("callback") String callback) {
// "log" the call
System.out.println("[" + new Date() + "] searching for: " + searchExp) ;
URL flickrUrl = getSearchURL(searchExp) ;
JSONObject data = tryToGetSearchResult(flickrUrl);
return new JSONWithPadding(data, callback);
}
private URL getSearchURL(String searchExp) {
String composedURL = FLICKR_API + searchExp;
try {
return new URL(composedURL);
} catch (MalformedURLException e) {
e.printStackTrace();
throw new RuntimeException("URL composition failed. Please check your URL: " + composedURL, e) ;
}
}
private JSONObject tryToGetSearchResult(URL flickrUrl) {
try {
return getSearchResult(flickrUrl) ;
} catch (IOException | JSONException e) {
e.printStackTrace();
throw new RuntimeException("Failed searching.", e) ;
}
}
private JSONObject getSearchResult(URL flickrUrl) throws IOException, JSONException {
InputStream is = flickrUrl.openStream() ;
String result = readDataFromStream(is);
// Flickr specific string prefix for the JSON feed.
if(result.indexOf("jsonFlickrFeed(") >= 0) {
result = result.substring("jsonFlickrFeed(".length(), result.length()-1) ;
return new JSONObject(result) ;
} else {
return new JSONObject("{}") ;
}
}
private String readDataFromStream(InputStream is) throws IOException {
StringBuilder data = new StringBuilder() ;
byte[] buffer = new byte[BUFFER_SIZE_IN_BYTES] ;
int bytesRead = is.read(buffer) ;
while(bytesRead != -1) {
data.append(new String(buffer, 0, bytesRead)) ;
bytesRead = is.read(buffer) ;
}
is.close() ;
return data.toString() ;
}
}
That's it. This our REST service, and we are exposing our API by using the @Path annotations
on the class and on the public method getPhotos. There are also other annotations such as the
@GET annotation and the @Produces annotation which state that the method is invoked on
an HTTP GET, and returns (@Produces annotation) a JSON (with Padding eventually = JSONP) object.
As you can see, in the class, we are using a Flickr API to get our images data from.
Now all that's left in order to make this service work, is to define the Jersey servlet in the web.xml file,
which is done this way:
<web-app>
...
<servlet>
<servlet-name>Jersey REST Service</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>quickpik.server.web</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Jersey REST Service</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
...
</web-app>
After doing that, we're able to refer to our REST service by using a URL in the browser:
http://localhost:8080/<web-app-name>/rest/quickpik/searchPhotos?searchExp=<our-search-term>
In order to verify this worked indeed and to get a better understanding of how the strucutre of our JSON result looks like, before even writing code to the GWT Chrome extension client,
i did a small test using jQuery, to see whether this worked. - You can find it in the GitHub repository.
What I did in a nutshell, was to call the REST API:
$.getJSON('http://localhost:8080/qpserver/rest/quickpik/searchPhotos?searchExp=hello&callback=?', jsonCB);
So I executed an AJAX call to search for "hello", and passed a callback - jsonCB to process the result.
jsonCB looks like this:
function jsonCB(result) {
console.log(result);
if (result !== null && result) {
if (result.items.length === 0) {
$($("#serverResponses")[0]).append("No results.")
} else {
for (item in result.items) {
var url = result.items[item].media.m;
var imgItem = document.createElement('img');
imgItem.src = url;
$($("#serverResponses")[0]).append("Server response: ")
.append(url).append("<br>");
$($("#serverResponses")[0]).append(imgItem).append("<br>");
}
}
}
}
Note the highlighted text! - This is how we get the URL information of the photo items being sent back to us from the server.
So now we know that inside "result" which is a JSON object, resides an array of objects, that contain each
an object called media with a field called m.
You can discover this by simply debugging the returned values from the Server in the browser.
So now, we can finally add some code to our GWT Client.
Client side - GWT Chrome extension
In the previous posts of QuickPik, you might have noticed, that in order to add an additional ImageDataSource, all you need is to implement an Interface IImagesDataSource and add that ImageDataSource to the DataSource enum.
It's much easier than it sounds.
So we need to do the following:
1. Write an ImageDataSource that "talks" to our Server.
2. Edit the manifest.json file to allow communication to our server in order to comply with the
"Content Security Policy" of Chrome's extensions.
IImageDataSource implementation - ServerDS
public class ServerDS implements IImagesDataSource {
private final static String QUICKPIK_SERVER_URL =
"http://localhost:8080/qpserver/rest/quickpik/searchPhotos?searchExp=" ;
...
@Override
public void getImages(final String searchExpression, final FilterLevel filter,
final Callback<PhotosSearchResult, Void> callback) {
String url = QUICKPIK_SERVER_URL + searchExpression ;
JsonpRequestBuilder jsonp = new JsonpRequestBuilder();
jsonp.requestObject(url, new AsyncCallback<ServerResult>() {
public void onFailure(Throwable throwable) {
// do some error handling..
}
public void onSuccess(ServerResult result) {
handleLoadedImagesResult(searchExpression, filter, result, callback) ;
}
});
}
private void handleLoadedImagesResult(String searchExpression, FilterLevel filter, ServerResult result,
Callback<PhotosSearchResult, Void> callback) {
JsArray<ServerImageItem> imageItems = result.getItems();
LinkedList<Photo> photos = collectImages(imageItems);
callback.onSuccess(new PhotosSearchResult(searchExpression, filter, photos, 0, false)) ;
}
private LinkedList<Photo> collectImages(JsArray<ServerImageItem> imageItems) {
LinkedList<Photo> photos = new LinkedList<Photo>();
for (int i = 0; i < imageItems.length(); i++) {
ServerImageItem imageItem = imageItems.get(i);
Photo p = new Photo(i+"", imageItem.getImageURL(), imageItem.getImageURL()) ;
photos.add(p) ;
}
return photos;
}
}
I'll try to explain the code very shortly:
1. At the top of the class you can locate the URL to the server we will be using to get the images from.
2. getImages method which must be implemented is passed a searchExpression a filter and
a callback. We will ignore the filter to make things simpler.
GWT offers a class called JsonpRequestBuilder which allows us to invoke an asynchrounous
call to the server, and passing a callback to deal with the result of the invocation.
3. If the call was successful, we have to handle the result somehow. We do that by calling a handle method.
4. In the handleLoadedImagesResult method, we got an object called ServerResult.
ServerResult is a custom object created especially for this operation. It is a wrapper to
a JavaScriptObject, that gives access to the result.items object array. (Remember from the jQuery test?)
This is how ServerResult looks like:
public class ServerResult extends JavaScriptObject {
protected ServerResult() {
}
public final native JsArray<ServerImageItem> getItems() /*-{
return this.items ;
}-*/;
}
JavaScript code "return this.items" is executed, returning the JavaScript items Array.
I also mapped the objects in this array to a Java object in the same way I did with ServerResult.
Following the same idea, ServerImageItem looks like this:
public class ServerImageItem extends JavaScriptObject {
protected ServerImageItem() {
}
public final native String getImageURL() /*-{
return this.media.m ;
}-*/;
}
Here we use getImageURL method to return the (JavaScript) object's media.m property.
(This is exactly the same structure that was referred to when testing with jQuery above).
So after this long explanation, all we do in the "handle" method, is basically gathering all of our
images URLs, creating a Photo list, like expected in the callback passed to us in the getImages method
invocation and invoking the callback, passing it an expected PhotoSearchResult object containing
our Photos.
5. Now we must add our ServerDS to the DataSource enum, so it will be included as a data source
when we run a search.
We do that simply by adding the (bolded) line below:
public enum DataSource {
// add here your data sources
FLICKR(false, new FlickrDS()),
QUICKPIK_SERVER(true, new ServerDS())
;
....
}
// add here your data sources
FLICKR(false, new FlickrDS()),
QUICKPIK_SERVER(true, new ServerDS())
;
....
}
Notice how I intentionally turned off FLICKR Data Source, setting its isEnabled flag to false.
6. Last thing that needs to be done, is to enable our Chrome extension to make calls to our server by
editing the manifest.json file, adding the following line to it:
{
....
// A relaxed policy definition which allows script resources to be loaded from localhost:8080 over HTTP
"content_security_policy": "script-src 'self' http://localhost:8080; object-src 'self'"
}
....
// A relaxed policy definition which allows script resources to be loaded from localhost:8080 over HTTP
"content_security_policy": "script-src 'self' http://localhost:8080; object-src 'self'"
}
That's all folks, all you need to do now, is build the Server (I did it with Gradle in this project), compile
your GWT client, deploy it on Chrome and watch it work.
You can access all the code on GitHub, and download it.
Code on GitHub
All of the project's code can be found on GitHub at: https://github.com/nirgit/Quickpik-with-server
There are several ways to communicate with a server.
ReplyDeleteLoading Arms