Presentation from GWT.create 2015 on how to easily build a REST API that can be consumed via GWT or native mobile clients. Presentation video here: http://gwtcreate.com/videos/#rpc-jersey-resty-gwt
4. map JSON
public class YqlResponse {
public YqlQuery query;
public static class YqlQuery {
public YqlResults results;
public static class YqlResults {
public List<Quote> quote;
}
}
}
no annotations needed
5. create a service API
@Path("http://query.yahooapis.com/v1/public")
public interface QuoteService extends RestService {
@GET
@Path("yql")
public void get(
@QueryParam("q") String query,
@QueryParam("env") String env,
@QueryParam("format") String format,
MethodCallback<YqlResponse> callback);
}
6. invoke the service
private static final QuoteService svc = GWT.create(QuoteService.class);
@Override
public void load(LoaderDemo.QuoteRequest input, final Callback<ListLoadResult<Quote>,
Throwable> callback) {
svc.get(input.getYql(), ENV, FORMAT, new MethodCallback<YqlResponse>() {
@Override
public void onFailure(Method method, Throwable throwable) {
Info.display("QuoteProxy", "failure");
callback.onFailure(throwable);
}
@Override
public void onSuccess(Method method, YqlResponse yqlResponse) {
List<Quote> quotes = yqlResponse.query.results.quote;
Info.display("QuoteProxy", "success");
callback.onSuccess(new ListLoadResultBean<Quote>(quotes));
}
});
}
7. simple CRUD API
public interface RestApi<T> extends RestService {
@GET
@Path("get")
public void get(@QueryParam("id")Long id, MethodCallback<T> callback);
@GET
@Path("all")
public void listAll(MethodCallback<ListResponse<T>> callback);
@POST
@Path("save")
public void save(T obj, MethodCallback<T> callback);
@POST
@Path("saveMany")
public void saveMany(List<T> obj, MethodCallback<Integer> callback);
@POST
@Path("delete")
public void delete(Long id, MethodCallback<Integer> callback);
}
8. not much here!
minimal boilerplate
@Path("/api/note")
public interface NoteItemRestService extends RestApi<Note> { }
private static final NoteItemRestService service = GWT.create(NoteItemRestService.class);
public void addNote(Display display, long listId, Note item)
{
NoteList noteList = App.getNoteListService().getNoteList(listId);
// All are 0-based for consistency with GWT constants
item.listId = listId;
service.save(item, new AppCallback<Note>(display) {
@Override
public void handleSuccess(Note result) {
App.getAppModel().getAllNotes().add(0, result);
App.getEventBus().fireEvent(new ShowMessageEvent("Note saved."));
App.getEventBus().fireEvent(new
NotesLoadedEvent(App.getAppModel().getAllNotes()));
App.getEventBus().fireEvent(new NoteAddedEvent(result));
}
});
}
9. public class ProjectEntryPoint implements EntryPoint {
private static DispatcherFactory dispatcherFactory = new DispatcherFactory();
private static FilterawareDispatcher dispatcher =
dispatcherFactory.cachingDispatcher();
@Override
public void onModuleLoad() {
// Configure RestyGWT
dispatcher.addFilter(new CORSFilter());
Defaults.setDispatcher(dispatcher);
LoaderDemo loaderDemo = new LoaderDemo();
RootPanel.get().add(loaderDemo);
}
}
ListAllNotesAction, ListAllNotesResult,
AddNoteAction, AddNoteResult, …
Command pattern
10. server side
@Path("api/note")
public class NoteDao extends RestServiceDao<Note>
{
private static final Logger LOG = Logger.getLogger(NoteDao.class.getName());
@Override
@Path("all")
@GET
public ListWrapper<Note> findAll() {
User user = AuthFilter.getUser();
List<Note> notes = this.listByOwner(user);
return new ListWrapper<Note>(notes);
}
. . .
}
11. Objectify + Jersey
@Produces(MediaType.APPLICATION_JSON)
public class RestServiceDao<T extends Owned> extends ObjectifyDao<T>
{
public T getForOwner() {
User user = AuthFilter.getUser();
T obj = null;
try {
obj = this.getByOwner(user);
return obj;
} catch (TooManyResultsException e) {
throw new WebApplicationException(e);
}
}
public ListWrapper<T> findAll() {
User user = AuthFilter.getUser();
List<T> userAll = this.listByOwner(user);
return new ListWrapper<T>(userAll);
}
. . .
}
12. getting Jersey
pom.xml
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<!-- if your container implements Servlet API older than 3.0,
use "jersey-container-servlet-core" -->
<artifactId>jersey-container-servlet-core</artifactId>
<version>2.7</version>
</dependency>
<!-- workaround for https://java.net/jira/browse/JERSEY-2459 -->
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>hk2-api</artifactId>
<version>2.3.0-b09</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.3.2</version>
</dependency>
14. what about auth?
AuthFilter.java
// if an API call, return JSON response
if (path.startsWith("/listmaker/api")) {
((HttpServletResponse) resp).setStatus(401);
resp.setContentType(MediaType.TEXT_PLAIN);
resp.getWriter().write("User must log in");
} else {
// otherwise redirect
httpRes.sendRedirect(LOGIN_FORM);
}
<filter>
<filter-name>AuthFilter</filter-name>
<filter-class>com.turbomanage.gwt.server.servlet.AuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AuthFilter</filter-name>
<url-pattern>/listmaker/*</url-pattern>
</filter-mapping>
15. exception handling
@Override
public void onFailure(Method method, Throwable throwable) {
String url = method.builder.getUrl();
App.getLogger().log(Level.SEVERE, "Error calling service " + url, throwable);
try {
// Decode the exception
if (throwable instanceof FailedStatusCodeException) {
FailedStatusCodeException sce = (FailedStatusCodeException) throwable;
App.getLogger().log(Level.SEVERE, "Service returned " + sce.getStatusCode() +
sce.getMessage());
if (401 == sce.getStatusCode()) {
Window.Location.replace(LOGIN_FORM);
} else if (500 == sce.getStatusCode()) {
if ("UserNotRegisteredException".equals(sce.getMessage())) {
Window.Location.replace(SIGNUP_URL);
}
}
}
handleFailure(throwable);
} finally {
reset(null);
}
}
16. application error handling
service.save(item, new AppCallback<Note>(display) {
@Override
public void handleSuccess(Note result) {
. . .
}
});
public abstract class AppCallback<R> implements MethodCallback<R> {
private final Display display;
public AppCallback() {
this.display = null;
}
public AppCallback(Display display) {
this.display = display;
display.startProcessing();
}
. . .
}
17. finer points
Text, JSON, XML via direct API — res.get()…
CachingRetryingDispatcher
ModelChangeEvent
Objects with final fields (@JsonCreator)
Polymorphism (@JsonSubTypes)
18. same obj on client / server?
older versions of RestyGWT used Jackson 1.7
RestyGWT 2.0 uses Jackson 2.3
so you can now use the same annotations on client
and server
and therefore the same POJOs (yeah)!
use @JsonIgnore for server-only fields (like Ref)
19. Please rate this session at
gwtcreate.com/agenda
src
github.com/turbomanage/listmaker
David Chandler
turbomanage.wordpress.com
resty-gwt.github.io