/* * Copyright (c) 2015 EMC Corporation * All Rights Reserved */ package storageapi; import java.util.concurrent.ExecutionException; import org.apache.commons.lang.StringUtils; import play.Logger; import play.libs.F; import play.libs.WS; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.ning.http.util.UTF8UrlEncoder; import static com.emc.vipr.client.impl.Constants.AUTH_TOKEN_KEY; /** * JSON WS API wrapper for accessing the backed storage APIs. This wrapper adds parsing to the JSON, basic logging and * the auth token support. * * @author Jonny Miller * @author Chris Dail */ public class JsonAPI { public static final String TIMEOUT = "10min"; protected ApiUrlFactory apiUrlFactory; protected Gson gson; public JsonAPI(ApiUrlFactory apiUrlFactory) { this.apiUrlFactory = apiUrlFactory; this.gson = new GsonBuilder().serializeNulls().create(); } // POST with no payload protected <T> F.Promise<APIResponse<T>> post(String authToken, Class<T> responseType, String path, String... args) { return post(authToken, null, responseType, path, args); } // Standard POST protected <T> F.Promise<APIResponse<T>> post(String authToken, Object request, final Class<T> responseType, String path, String... args) { WS.WSRequest wsReq = newRequest(authToken, request, path, args); Logger.info("POST: %s, %s", wsReq.url, wsReq.body); return responseHandler(wsReq.postAsync(), responseType); } // POST with no response protected F.Promise<APIResponse<Boolean>> post(String authToken, Object request, String path, String... args) { WS.WSRequest wsReq = newRequest(authToken, request, path, args); Logger.info("POST: %s, %s", wsReq.url, wsReq.body); return responseHandler(wsReq.postAsync()); } // DELETE with no payload protected F.Promise<APIResponse<Boolean>> delete(String authToken, String path, String... args) { WS.WSRequest wsReq = newRequest(authToken, null, path, args); Logger.info("DELETE: %s, %s", wsReq.url, wsReq.body); return responseHandler(wsReq.deleteAsync()); } // PUT with no payload protected F.Promise<APIResponse<Boolean>> put(String authToken, String path, String... args) { WS.WSRequest wsReq = newRequest(authToken, null, path, args); Logger.info("PUT: %s, %s", wsReq.url, wsReq.body); return responseHandler(wsReq.putAsync()); } // PUT with payload protected <T> F.Promise<APIResponse<T>> put(String authToken, Object request, final Class<T> responseType, String path, String... args) { WS.WSRequest wsReq = newRequest(authToken, request, path, args); Logger.info("PUT: %s, %s", wsReq.url, wsReq.body); return responseHandler(wsReq.putAsync(), responseType); } // GET protected <T> F.Promise<APIResponse<T>> get(String authToken, final Class<T> responseType, String path, String... args) { WS.WSRequest wsReq = newRequest(authToken, null, path, args); Logger.info("GET: %s, %s", wsReq.url, wsReq.body); return responseHandler(wsReq.getAsync(), responseType); } protected <T> APIResponse<T> readJson(WS.HttpResponse response, Class<T> type) { if (response.success()) { return new APIResponse<T>(response.getStatus(), gson.fromJson(response.getJson(), type)); } if (StringUtils.isBlank(response.getStatusText())) { return new APIResponse<T>(response.getStatus(), null); } else { return new APIResponse<T>(new APIException(response.getStatus(), response.getStatusText(), response.getString())); } } // The default play encode() function uses the Java URLEncoder. This is wrong. Query parameters must be encoded // using RFC-3986 which is not the same. protected String encodeUrl(String template, String... params) { Object[] encodedParams = new String[params.length]; for (int i = 0; i < params.length; i++) { encodedParams[i] = UTF8UrlEncoder.encode(params[i]); } return String.format(template, encodedParams); } /** * Creates a new WSRequest to the API using the given path. the request is added parsed as JSON. * * @param authToken Auth token sent as the Security.AUTH_HEADER * @param path Path to the API * @param request Request object (can be null) * @return WS.WSRequest */ protected WS.WSRequest newRequest(String authToken, Object request, String path, String... args) { WS.WSRequest wsReq = WS.url(encodeUrl(apiUrlFactory.getUrl() + path, args)); wsReq.timeout(TIMEOUT); if (authToken != null) { wsReq.setHeader(AUTH_TOKEN_KEY, authToken); } wsReq.setHeader("Accept", "application/json"); if (request != null) { String json = gson.toJson(request); wsReq.setHeader("Content-Type", "application/json"); wsReq.body(json); } return wsReq; } /** * Creates a wrapper around a promise result of the WS request. All this does is log the output. */ protected F.Promise<APIResponse<Boolean>> responseHandler(F.Promise<WS.HttpResponse> wsRespPromise) { final F.Promise<APIResponse<Boolean>> methodResult = new F.Promise<APIResponse<Boolean>>(); wsRespPromise.onRedeem(new F.Action<F.Promise<WS.HttpResponse>>() { @Override public void invoke(F.Promise<WS.HttpResponse> result) { try { WS.HttpResponse wsResp = result.get(); Logger.info("API RESP: %d, %s", wsResp.getStatus(), wsResp.getString()); methodResult.invoke(new APIResponse(wsResp.getStatus(), wsResp.success())); } catch (ExecutionException e) { Logger.error(e, "API Exception"); methodResult.invoke(new APIResponse(e.getCause())); } catch (InterruptedException e) { Logger.error(e, "API Exception"); methodResult.invoke(new APIResponse(e)); } catch (RuntimeException e) { Logger.error(e, "API Exception"); methodResult.invoke(new APIResponse(e)); } } }); return methodResult; } /** * Wraps a promise response with a new promise. This one parses the result as JSON returning the JSON to the new * promise. * * @param wsRespPromise WS Response * @param responseType Class to parse JSON * @param <T> Type of the request * @return A new promise that will return the parsed JSON */ protected <T> F.Promise<APIResponse<T>> responseHandler(F.Promise<WS.HttpResponse> wsRespPromise, final Class<T> responseType) { final F.Promise<APIResponse<T>> methodResult = new F.Promise<APIResponse<T>>(); wsRespPromise.onRedeem(new F.Action<F.Promise<WS.HttpResponse>>() { @Override public void invoke(F.Promise<WS.HttpResponse> result) { try { WS.HttpResponse wsResp = result.get(); Logger.info("API RESP: %d, %s", wsResp.getStatus(), wsResp.getString()); methodResult.invoke(readJson(wsResp, responseType)); } catch (ExecutionException e) { handleAPIException(e.getCause()); } catch (InterruptedException e) { handleAPIException(e); } catch (APIException e) { handleAPIException(e); } // GSON exceptions are runtime exceptions catch (RuntimeException e) { handleAPIException(e); } catch (Exception e) { handleAPIException(e); } } private void handleAPIException(Throwable e) { Logger.error(e, "API Exception"); methodResult.invoke(new APIResponse<T>(e)); } }); return methodResult; } }