/*******************************************************************************
*
* Copyright 2011-2014 Spiffy UI Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
package org.spiffyui.client.rest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.spiffyui.client.JSONUtil;
import org.spiffyui.client.JSUtil;
import org.spiffyui.client.MessageUtil;
import org.spiffyui.client.i18n.SpiffyUIStrings;
import org.spiffyui.client.rest.v2.RESTOAuthProvider;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.Window;
/**
* A set of utilities for calling REST from GWT.
*/
public final class RESTility
{
private static final SpiffyUIStrings STRINGS = (SpiffyUIStrings) GWT.create(SpiffyUIStrings.class);
private static final String LOCALE_COOKIE = "Spiffy_Locale";
private static final RESTility RESTILITY = new RESTility();
/**
* This method represents an HTTP GET request
*/
public static final HTTPMethod GET = RESTILITY.new HTTPMethod("GET");
/**
* This method represents an HTTP PUT request
*/
public static final HTTPMethod PUT = RESTILITY.new HTTPMethod("PUT");
/**
* This method represents an HTTP POST request
*/
public static final HTTPMethod POST = RESTILITY.new HTTPMethod("POST");
/**
* This method represents an HTTP DELETE request
*/
public static final HTTPMethod DELETE = RESTILITY.new HTTPMethod("DELETE");
private static boolean g_inLoginProcess = false;
private static List<RESTLoginCallBack> g_loginListeners = new ArrayList<RESTLoginCallBack>();
private int m_callCount = 0;
private boolean m_hasLoggedIn = false;
private boolean m_logInListenerCalled = false;
private boolean m_secureCookies = false;
private String m_sessionCookie = "Spiffy_Session";
private static RESTAuthProvider g_authProvider;
private static RESTOAuthProvider g_oAuthProvider;
private String m_sessionCookiePath;
/**
* This is a helper type class so we can pass the HTTP method as a type safe
* object instead of a string.
*/
public final class HTTPMethod
{
private String m_method;
/**
* Create a new HTTPMethod object
*
* @param method the method
*/
private HTTPMethod(String method)
{
m_method = method;
}
/**
* Get the method string for this object
*
* @return the method string
*/
private String getMethod()
{
return m_method;
}
}
private Map<RESTCallback, RESTCallStruct> m_restCalls = new HashMap<RESTCallback, RESTCallStruct>();
private String m_userToken = null;
private String m_tokenType = null;
private String m_tokenServerUrl = null;
private String m_username = null;
private String m_tokenServerLogoutUrl = null;
private String m_bestLocale = null;
/**
* Just to make sure that nobody else can instatiate this object.
*/
private RESTility()
{
}
private static final String URI_KEY = "uri";
private static final String SIGNOFF_URI_KEY = "signoffuri";
static {
RESTAuthProvider authUtil = new AuthUtil();
RESTility.setAuthProvider(authUtil);
}
/**
* <p>
* Sets the authentication provider used for future REST requests.
* </p>
*
* <p>
* By default authentication is provided by the AuthUtil class, but this
* class may be replaced to provide support for custom authentication schemes.
* </p>
*
* @param authProvider
* the new authentication provider
*
* @see AuthUtil
*/
public static void setAuthProvider(RESTAuthProvider authProvider)
{
g_authProvider = authProvider;
}
/**
* Set the OAuth provider for this application.
*
* @param oAuthProvider
* the oAuth provider
*/
public static void setOAuthProvider(RESTOAuthProvider oAuthProvider)
{
g_oAuthProvider = oAuthProvider;
}
/**
* <p>
* Sets the name of the Spiffy UI session cookie.
* </p>
*
* <p>
* Spiffy UI uses a local cookie to save the current user token. This cookie has
* the name <code>Spiffy_Session</code> by default. This method will change the
* name of the cookie to something else.
* </p>
*
* <p>
* Calling this method will not change the name of the cookie until a REST call is made
* which causes the cookie to be reset.
* </p>
*
* @param name the name of the cookie Spiffy UI session cookie
*/
public static void setSessionCookieName(String name)
{
if (name == null) {
throw new IllegalArgumentException("The session cookie name must not be null");
}
RESTILITY.m_sessionCookie = name;
}
/**
* Gets the current name of the Spiffy UI session cookie.
*
* @return the name of the session cookie
*/
public static String getSessionCookieName()
{
return RESTILITY.m_sessionCookie;
}
/**
* <p>
* Sets if RESTility cookies should only be sent via SSL.
* </p>
*
* <p>
* RESTility stores a small amount of information locally so users can refresh the page
* without logging out or resetting their locale. This information is sometimes stored
* in cookies. These cookies are normally not sent back to the server, but can be in
* some cases.
* </p>
*
* <p>
* If your application is concerned with sercurity you might want to require all cookies
* are only sent via a secure connection (SSL). Set this method true to make all cookies
* stored be RESTility and Spiffy UI require an SSL connection before they are sent.
* </p>
*
* <p>
* The default value of this field is false.
* </p>
*
* @param secure true if cookies should require SSL and false otherwise
*/
public static void setRequireSecureCookies(boolean secure)
{
RESTILITY.m_secureCookies = secure;
}
/**
* Determines if RESTility will force all cookies to require SSL.
*
* @return true if cookies require SSL and false otherwise
*/
public static boolean requiresSecureCookies()
{
return RESTILITY.m_secureCookies;
}
/**
* Gets the current auth provider which will be used for future REST calls.
*
* @return The current auth provider
*/
public static RESTAuthProvider getAuthProvider()
{
return g_authProvider;
}
/**
* Make a login request using RESTility authentication framework.
*
* @param callback the rest callback called when the login is complete
* @param response the response from the server requiring the login
* @param url the URL of the authentication server
* @param errorCode the error code from the server returned with the 401
*
* @exception RESTException
* if there was an exception when making the login request
*/
public static void login(RESTCallback callback, Response response, String url, String errorCode)
throws RESTException
{
RESTILITY.doLogin(callback, response, url, errorCode, null);
}
/**
* Make a login request using RESTility authentication framework.
*
* @param callback the rest callback called when the login is complete
* @param response the response from the server requiring the login
* @param url the URL of the authentication server
* @param exception the RESTException which prompted this login
*
* @exception RESTException
* if there was an exception when making the login request
*/
public static void login(RESTCallback callback, Response response, String url, RESTException exception)
throws RESTException
{
RESTILITY.doLogin(callback, response, url, null, exception);
}
private static String trimQuotes(String header)
{
if (header == null) {
return header;
}
String ret = header;
if (ret.startsWith("\"")) {
ret = ret.substring(1);
}
if (ret.endsWith("\"")) {
ret = ret.substring(0, ret.length() - 1);
}
return ret;
}
private void doLogin(RESTCallback callback, Response response, String url, String errorCode, RESTException exception)
throws RESTException
{
/*
When the server returns a status code 401 they are required
to send back the WWW-Authenticate header to tell us how to
authenticate.
*/
String auth = response.getHeader("WWW-Authenticate");
if (auth == null) {
throw new RESTException(RESTException.NO_AUTH_HEADER,
"", STRINGS.noAuthHeader(),
new HashMap<String, String>(),
response.getStatusCode(),
url);
}
/*
* Now we have to parse out the token server URL and other information.
*
* The String should look like this:
*
* X-OPAQUE uri=<token server URI>,signoffUri=<token server logout url>
*
* First we'll remove the token type
*/
String tokenType = auth;
String loginUri = null;
String logoutUri = null;
if (tokenType.indexOf(' ') != -1) {
tokenType = tokenType.substring(0, tokenType.indexOf(' ')).trim();
auth = auth.substring(auth.indexOf(' ') + 1);
if (auth.indexOf(',') != -1) {
String props[] = auth.split(",");
for (String prop : props) {
if (prop.trim().toLowerCase().startsWith(URI_KEY)) {
loginUri = prop.substring(prop.indexOf('=') + 1, prop.length()).trim();
} else if (prop.trim().toLowerCase().startsWith(SIGNOFF_URI_KEY)) {
logoutUri = prop.substring(prop.indexOf('=') + 1, prop.length()).trim();
}
}
loginUri = trimQuotes(loginUri);
logoutUri = trimQuotes(logoutUri);
if (logoutUri.trim().length() == 0) {
logoutUri = loginUri;
}
}
}
setTokenType(tokenType);
setTokenServerURL(loginUri);
setTokenServerLogoutURL(logoutUri);
if (RESTILITY.m_sessionCookiePath != null) {
removeCookie(RESTILITY.m_sessionCookie, RESTILITY.m_sessionCookiePath);
} else {
removeCookie(RESTILITY.m_sessionCookie);
}
removeCookie(LOCALE_COOKIE);
if (g_oAuthProvider != null && tokenType.equalsIgnoreCase("Bearer") || tokenType.equalsIgnoreCase("MAC")) {
handleOAuthRequest(callback, loginUri, response, exception);
} else if (g_authProvider instanceof org.spiffyui.client.rest.v2.RESTAuthProvider) {
((org.spiffyui.client.rest.v2.RESTAuthProvider) g_authProvider).showLogin(callback, loginUri, response, exception);
} else {
g_authProvider.showLogin(callback, loginUri, errorCode);
}
}
private void handleNoPrivilege(RESTException exception)
{
if (g_oAuthProvider != null && exception != null) {
/*
* This is a special OAuth state that Spiffy UI recognizes. It means that
* the user has supplied valid credentials, but they don't have access and
* further calls will only result in a 401.
*
* There isn't much we can do in this case so we just pass the exception to
* the OAuth provider to handle it.
*/
g_oAuthProvider.error(exception);
}
}
private void handleOAuthRequest(RESTCallback callback, String tokenServerUrl, Response response, RESTException exception)
throws RESTException
{
String authUrl = g_oAuthProvider.getAuthServerUrl(callback, tokenServerUrl, response, exception);
if (exception != null && AuthUtil.NO_PRIVILEGE.equals(exception.getCode())) {
/*
* This is a special OAuth state that Spiffy UI recognizes. It means that
* the user has supplied valid credentials, but they don't have access and
* further calls will only result in a 401.
*
* There isn't much we can do in this case so we just pass the exception to
* the OAuth provider to handle it.
*/
g_oAuthProvider.error(exception);
} else {
handleOAuthRequestJS(this, authUrl, g_oAuthProvider.getClientId(),
g_oAuthProvider.getScope(), g_oAuthProvider.shouldSendRedirectUrl());
}
}
private void oAuthComplete(String token, String tokenType)
{
setTokenType(tokenType);
setUserToken(token);
finishRESTCalls();
}
private native String base64Encode(String s) /*-{
return $wnd.Base64.encode(s);
}-*/;
private native void handleOAuthRequestJS(RESTility callback, String authUrl, String clientId, String scope, boolean shouldRedirect) /*-{
$wnd.spiffyui.oAuthAuthenticate(authUrl, clientId, scope, shouldRedirect, function(token, tokenType) {
callback.@org.spiffyui.client.rest.RESTility::oAuthComplete(Ljava/lang/String;Ljava/lang/String;)(token,tokenType);
});
}-*/;
/**
* Returns HTTPMethod corresponding to method name.
* If the passed in method does not match any, GET is returned.
*
* @param method a String representation of a http method
* @return the HTTPMethod corresponding to the passed in String method representation.
*/
public static HTTPMethod parseString(String method)
{
if (POST.getMethod().equalsIgnoreCase(method)) {
return POST;
} else if (PUT.getMethod().equalsIgnoreCase(method)) {
return PUT;
} else if (DELETE.getMethod().equalsIgnoreCase(method)) {
return DELETE;
} else {
//Otherwise return GET
return GET;
}
}
/**
* Upon logout, delete cookie and clear out all member variables
*/
public static void doLocalLogout()
{
RESTILITY.m_hasLoggedIn = false;
RESTILITY.m_logInListenerCalled = false;
RESTILITY.m_callCount = 0;
RESTILITY.m_userToken = null;
RESTILITY.m_tokenType = null;
RESTILITY.m_tokenServerUrl = null;
RESTILITY.m_tokenServerLogoutUrl = null;
RESTILITY.m_username = null;
if (RESTILITY.m_sessionCookiePath != null) {
removeCookie(RESTILITY.m_sessionCookie, RESTILITY.m_sessionCookiePath);
} else {
removeCookie(RESTILITY.m_sessionCookie);
}
removeCookie(LOCALE_COOKIE);
}
/**
* The normal GWT mechanism for removing cookies will remove a cookie at the path
* the page is on. The is a possibility that the session cookie was set on the
* server with a slightly different path. In that case we need to try to delete
* the cookie on all the paths of the current URL. This method handles that case.
*
* @param name the name of the cookie to remove
*/
private static void removeCookie(String name)
{
Cookies.removeCookie(name);
if (Cookies.getCookie(name) != null) {
/*
* This could mean that the cookie was there,
* but was on a different path than the one that
* we get by default.
*/
removeCookie(name, Window.Location.getPath());
}
}
private static void removeCookie(String name, String currentPath)
{
Cookies.removeCookie(name, currentPath);
if (Cookies.getCookie(name) != null) {
/*
* This could mean that the cookie was there,
* but was on a different path than the one that
* we were passed. In that case we'll bump up
* the path and try again.
*/
String path = currentPath;
if (path.charAt(0) != '/') {
path = "/" + path;
}
int slashloc = path.lastIndexOf('/');
if (slashloc > 1) {
path = path.substring(0, slashloc);
removeCookie(name, path);
}
}
}
/**
* In some cases, like login, the original REST call returns an error and we
* need to run it again. This call gets the same REST request information and
* tries the request again.
*/
public static void finishRESTCalls()
{
fireLoginSuccess();
for (RESTCallback callback : RESTILITY.m_restCalls.keySet()) {
RESTCallStruct struct = RESTILITY.m_restCalls.get(callback);
if (struct != null && struct.shouldReplay()) {
callREST(struct.getOptions());
}
}
}
/**
* Make a rest call using an HTTP GET to the specified URL.
*
* @param callback the callback to invoke
* @param url the properly encoded REST url to call
*/
public static void callREST(String url, RESTCallback callback)
{
callREST(url, "", RESTility.GET, callback);
}
/**
* Make a rest call using an HTTP GET to the specified URL including
* the specified data..
*
* @param url the properly encoded REST url to call
* @param data the data to pass to the URL
* @param callback the callback to invoke
*/
public static void callREST(String url, String data, RESTCallback callback)
{
callREST(url, data, RESTility.GET, callback);
}
/**
* Set the user token in JavaScript memory and and saves it in a cookie.
* @param token user token
*/
public static void setUserToken(String token)
{
g_inLoginProcess = false;
RESTILITY.m_userToken = token;
setSessionToken();
}
/**
* Set the authentication server url in JavaScript memory and and saves it in a cookie.
* @param url authentication server url
*/
public static void setTokenServerURL(String url)
{
RESTILITY.m_tokenServerUrl = url;
setSessionToken();
}
/**
* Fire the login success event to all listeners if it hasn't been fired already.
*/
public static void fireLoginSuccess()
{
RESTILITY.m_hasLoggedIn = true;
if (!RESTILITY.m_logInListenerCalled) {
for (RESTLoginCallBack listener : g_loginListeners) {
listener.onLoginSuccess();
}
RESTILITY.m_logInListenerCalled = true;
}
}
/**
* <p>
* Set the type of token RESTility will pass.
* </p>
*
* <p>
* Most of the time the token type is specified by the REST server and the
* client does not have to specify this value. This method is mostly used
* for testing.
* </p>
*
* @param type the token type
*/
public static void setTokenType(String type)
{
RESTILITY.m_tokenType = type;
setSessionToken();
}
/**
* Set the user name in JavaScript memory and and saves it in a cookie.
* @param username user name
*/
public static void setUsername(String username)
{
RESTILITY.m_username = username;
setSessionToken();
}
/**
* Set the authentication server logout url in JavaScript memory and and saves it in a cookie.
* @param url authentication server logout url
*/
public static void setTokenServerLogoutURL(String url)
{
RESTILITY.m_tokenServerLogoutUrl = url;
setSessionToken();
}
/**
* We can't know the best locale until after the user logs in because we need to consider
* their locale from the identity vault. So we get the locale as part of the identity
* information and then store this in a cookie. If the cookie doesn't match the current
* locale then we need to refresh the page so we can reload the JavaScript libraries.
*
* @param locale the locale
*/
public static void setBestLocale(String locale)
{
if (getBestLocale() != null) {
if (!getBestLocale().equals(locale)) {
/*
If the best locale from the server doesn't
match the cookie. That means we set the cookie
with the new locale and refresh the page.
*/
RESTILITY.m_bestLocale = locale;
setSessionToken();
JSUtil.hide("body", "");
JSUtil.reload(true);
} else {
/*
If the best locale from the server matches
the best locale from the cookie then we are
done.
*/
return;
}
} else {
/*
This means they didn't have a best locale from
the server stored as a cookie in the client. So
we set the locale from the server into the cookie.
*/
RESTILITY.m_bestLocale = locale;
setSessionToken();
/*
* If there are any REST requests in process when we refresh
* the page they will cause errors that show up before the
* page reloads. Hiding the page makes those errors invisible.
*/
JSUtil.hide("body", "");
JSUtil.reload(true);
}
}
/**
* <p>
* Set the path for the Spiffy_Session cookie.
* </p>
*
* <p>
* When Spiffy UI uses token based authentication it saves token and user information
* in a cookie named Spiffy_Session. This cookie allows the user to remain logged in
* after they refresh the page the reset JavaScript memory.
* </p>
*
* <p>
* By default that cookie uses the path of the current page. If an application uses
* multiple pages it can make sense to use a more general path for this cookie to make
* it available to other URLs in the application.
* </p>
*
* <p>
* The path must be set before the first authentication request. If it is called
* afterward the cookie path will not change.
* </p>
*
* @param newPath the new path for the cookie or null if the cookie should use the path of the current page
*/
public static void setSessionCookiePath(String newPath)
{
RESTILITY.m_sessionCookiePath = newPath;
}
/**
* Get the path of the session cookie. By default this is null indicating a path of
* the current page will be used.
*
* @return the current session cookie path.
*/
public static String getSessionCookiePath()
{
return RESTILITY.m_sessionCookiePath;
}
private static void setSessionToken()
{
if (RESTILITY.m_sessionCookiePath != null) {
Cookies.setCookie(RESTILITY.m_sessionCookie, RESTILITY.m_tokenType + "," +
RESTILITY.m_userToken + "," + RESTILITY.m_tokenServerUrl + "," +
RESTILITY.m_tokenServerLogoutUrl + "," + RESTILITY.m_username,
null, null, RESTILITY.m_sessionCookiePath, RESTILITY.m_secureCookies);
if (RESTILITY.m_bestLocale != null) {
Cookies.setCookie(LOCALE_COOKIE, RESTILITY.m_bestLocale, null, null,
RESTILITY.m_sessionCookiePath, RESTILITY.m_secureCookies);
}
} else {
Cookies.setCookie(RESTILITY.m_sessionCookie, RESTILITY.m_tokenType + "," +
RESTILITY.m_userToken + "," + RESTILITY.m_tokenServerUrl + "," +
RESTILITY.m_tokenServerLogoutUrl + "," + RESTILITY.m_username,
null, null, null, RESTILITY.m_secureCookies);
if (RESTILITY.m_bestLocale != null) {
Cookies.setCookie(LOCALE_COOKIE, RESTILITY.m_bestLocale, null, null, null, RESTILITY.m_secureCookies);
}
}
}
/**
* Returns a boolean flag indicating whether user has logged in or not
*
* @return boolean indicating whether user has logged in or not
*/
public static boolean hasUserLoggedIn()
{
return RESTILITY.m_hasLoggedIn;
}
/**
* Returns user's full authentication token, prefixed with "X-OPAQUE"
*
* @return user's full authentication token prefixed with "X-OPAQUE"
*/
public static String getFullAuthToken()
{
return getTokenType() + " " + getUserToken();
}
private static void checkSessionCookie()
{
String sessionCookie = Cookies.getCookie(RESTILITY.m_sessionCookie);
if (sessionCookie != null && sessionCookie.length() > 0) {
// If the cookie value is quoted, strip off the enclosing quotes
if (sessionCookie.length() > 2 &&
sessionCookie.charAt(0) == '"' &&
sessionCookie.charAt(sessionCookie.length() - 1) == '"') {
sessionCookie = sessionCookie.substring(1, sessionCookie.length() - 1);
}
String sessionCookiePieces [] = sessionCookie.split(",");
if (sessionCookiePieces != null) {
if (sessionCookiePieces.length >= 1) {
RESTILITY.m_tokenType = sessionCookiePieces [0];
}
if (sessionCookiePieces.length >= 2) {
RESTILITY.m_userToken = sessionCookiePieces [1];
}
if (sessionCookiePieces.length >= 3) {
RESTILITY.m_tokenServerUrl = sessionCookiePieces [2];
}
if (sessionCookiePieces.length >= 4) {
RESTILITY.m_tokenServerLogoutUrl = sessionCookiePieces [3];
}
if (sessionCookiePieces.length >= 5) {
RESTILITY.m_username = sessionCookiePieces [4];
}
}
}
}
/**
* Returns user's authentication token
*
* @return user's authentication token
*/
public static String getUserToken()
{
/*
* We want to make sure that we use the token that's stored in
* the cookie since we want to tell if the user logged out in
* another browser tab.
* We start by deleting the token from memory.
*/
RESTILITY.m_userToken = null;
/*
* Then we try to get the token from the cookie.
*/
checkSessionCookie();
/*
* Then we return the cookie from the token if it's available.
*/
return RESTILITY.m_userToken;
}
/**
* Returns user's authentication token type
*
* @return user's authentication token type
*/
public static String getTokenType()
{
if (RESTILITY.m_tokenType == null) {
checkSessionCookie();
}
return RESTILITY.m_tokenType;
}
/**
* Returns the authentication server url
*
* @return authentication server url
*/
public static String getTokenServerUrl()
{
if (RESTILITY.m_tokenServerUrl == null) {
checkSessionCookie();
}
return RESTILITY.m_tokenServerUrl;
}
/**
* Returns authentication server logout url
*
* @return authentication server logout url
*/
public static String getTokenServerLogoutUrl()
{
if (RESTILITY.m_tokenServerLogoutUrl == null) {
checkSessionCookie();
}
return RESTILITY.m_tokenServerLogoutUrl;
}
/**
* Returns the name of the currently logged in user or null
* if the current user is not logged in.
*
* @return user name
*/
public static String getUsername()
{
if (RESTILITY.m_username == null) {
checkSessionCookie();
}
return RESTILITY.m_username;
}
/**
* Returns best matched locale
*
* @return best matched locale
*/
public static String getBestLocale()
{
if (RESTILITY.m_bestLocale != null) {
return RESTILITY.m_bestLocale;
} else {
RESTILITY.m_bestLocale = Cookies.getCookie(LOCALE_COOKIE);
return RESTILITY.m_bestLocale;
}
}
public static List<RESTLoginCallBack> getLoginListeners()
{
return g_loginListeners;
}
/**
* Make an HTTP call and get the results as a JSON object. This method handles
* cases like login, error parsing, and configuration requests.
*
* @param url the properly encoded REST url to call
* @param data the data to pass to the URL
* @param method the HTTP method, defaults to GET
* @param callback the callback object for handling the request results
*/
public static void callREST(String url, String data, RESTility.HTTPMethod method, RESTCallback callback)
{
callREST(url, data, method, callback, false, null);
}
/**
* Make an HTTP call and get the results as a JSON object. This method handles
* cases like login, error parsing, and configuration requests.
*
* @param url the properly encoded REST url to call
* @param data the data to pass to the URL
* @param method the HTTP method, defaults to GET
* @param callback the callback object for handling the request results
* @param etag the option etag for this request
*/
public static void callREST(String url, String data, RESTility.HTTPMethod method,
RESTCallback callback, String etag)
{
callREST(url, data, method, callback, false, etag);
}
/**
* The client can't really handle the test for all XSS attacks, but we can
* do some general sanity checking.
*
* @param data the data to check
*
* @return true if the data is valid and false otherwise
*/
private static boolean hasPotentialXss(final String data)
{
if (data == null) {
return false;
}
String uppercaseData = data.toUpperCase();
if (uppercaseData.indexOf("<SCRIPT") > -1) {
return true;
}
return false;
}
/**
* Make an HTTP call and get the results as a JSON object. This method handles
* cases like login, error parsing, and configuration requests.
*
* @param url the properly encoded REST url to call
* @param data the data to pass to the URL
* @param method the HTTP method, defaults to GET
* @param callback the callback object for handling the request results
* @param isLoginRequest
* true if this is a request to login and false otherwise
* @param etag the option etag for this request
*
*/
public static void callREST(String url, String data, RESTility.HTTPMethod method,
RESTCallback callback, boolean isLoginRequest, String etag)
{
callREST(url, data, method, callback, isLoginRequest, true, etag);
}
/**
* <p>
* Make an HTTP call and get the results as a JSON object. This method handles
* allows the caller to override any HTTP headers in the request.
* </p>
*
* <p>
* By default RESTility sets two HTTP headers: <code>Accept=application/json</code> and
* <code>Accept-Charset=UTF-8</code>. Other headers are added by the browser running this
* application.
* </p>
*
* @param url the properly encoded REST url to call
* @param data the data to pass to the URL
* @param method the HTTP method, defaults to GET
* @param callback the callback object for handling the request results
* @param etag the option etag for this request
* @param headers a map containing the headers to the HTTP request. Any item
* in this map will override the default headers.
*/
public static void callREST(String url, String data, RESTility.HTTPMethod method,
RESTCallback callback, String etag, Map<String, String> headers)
{
callREST(url, data, method, callback, false, true, etag, headers);
}
/**
* Make an HTTP call and get the results as a JSON object. This method handles
* cases like login, error parsing, and configuration requests.
*
* @param url the properly encoded REST url to call
* @param data the data to pass to the URL
* @param method the HTTP method, defaults to GET
* @param callback the callback object for handling the request results
* @param isLoginRequest
* true if this is a request to login and false otherwise
* @param shouldReplay true if this request should repeat after a login request
* if this request returns a 401
* @param etag the option etag for this request
*/
public static void callREST(String url, String data, RESTility.HTTPMethod method,
RESTCallback callback, boolean isLoginRequest,
boolean shouldReplay, String etag)
{
callREST(url, data, method, callback, isLoginRequest, shouldReplay, etag, null);
}
/**
* <p>
* Make an HTTP call and get the results as a JSON object. This method handles
* cases like login, error parsing, and configuration requests.
* </p>
*
* <p>
* By default RESTility sets two HTTP headers: <code>Accept=application/json</code> and
* <code>Accept-Charset=UTF-8</code>. Other headers are added by the browser running this
* application.
* </p>
*
* @param url the properly encoded REST url to call
* @param data the data to pass to the URL
* @param method the HTTP method, defaults to GET
* @param callback the callback object for handling the request results
* @param isLoginRequest
* true if this is a request to login and false otherwise
* @param shouldReplay true if this request should repeat after a login request
* if this request returns a 401
* @param etag the option etag for this request
* @param headers a map containing the headers to the HTTP request. Any item
* in this map will override the default headers.
*/
public static void callREST(String url, String data, RESTility.HTTPMethod method,
RESTCallback callback, boolean isLoginRequest,
boolean shouldReplay, String etag, Map<String, String> headers)
{
RESTOptions options = new RESTOptions();
options.setURL(url);
if (data != null && data.trim().length() > 0) {
options.setDataString(data);
}
options.setMethod(method);
options.setCallback(callback);
options.setIsLoginRequest(isLoginRequest);
options.setShouldReplay(shouldReplay);
options.setEtag(etag);
options.setHeaders(headers);
callREST(options);
}
/**
* <p>
* Make an HTTP call and get the results as a JSON object. This method handles
* cases like login, error parsing, and configuration requests.
* </p>
*
* @param options the options for the REST request
*/
public static void callREST(RESTOptions options)
{
if (hasPotentialXss(options.getDataString())) {
options.getCallback().onError(new RESTException(RESTException.XSS_ERROR,
"", STRINGS.noServerContact(),
new HashMap<String, String>(),
-1, options.getURL()));
return;
}
RESTILITY.m_restCalls.put(options.getCallback(), new RESTCallStruct(options));
RequestBuilder builder = new RESTRequestBuilder(options.getMethod().getMethod(), options.getURL());
/*
Set our headers
*/
builder.setHeader("Accept", "application/json");
builder.setHeader("Accept-Charset", "UTF-8");
if (options.getHeaders() != null) {
for (String k : options.getHeaders().keySet()) {
builder.setHeader(k, options.getHeaders().get(k));
}
}
if (RESTILITY.m_bestLocale != null) {
/*
* The REST end points use the Accept-Language header to determine
* the locale to use for the contents of the REST request. Normally
* the browser will fill this in with the browser locale and that
* doesn't always match the preferred locale from the Identity Vault
* so we need to set this value with the correct preferred locale.
*/
builder.setHeader("Accept-Language", RESTILITY.m_bestLocale);
}
if (getUserToken() != null &&
getTokenServerUrl() != null) {
builder.setHeader("Authorization", getFullAuthToken());
builder.setHeader("TS-URL", getTokenServerUrl());
}
if (options.getEtag() != null) {
builder.setHeader("If-Match", options.getEtag());
}
if (options.getDataString() != null) {
/*
Set our request data
*/
builder.setRequestData(options.getDataString());
if (builder.getHeader("Content-Type") == null) {
//b/c jaxb/jersey chokes when there is no data when content-type is json
builder.setHeader("Content-Type", options.getContentType());
}
}
builder.setCallback(RESTILITY.new RESTRequestCallback(options.getCallback()));
try {
/*
If we are in the process of logging in then all other
requests will just return with a 401 until the login
is finished. We want to delay those requests until
the login is complete when we will replay all of them.
*/
if (options.isLoginRequest() || !g_inLoginProcess) {
builder.send();
}
} catch (RequestException e) {
MessageUtil.showFatalError(e.getMessage());
}
}
/**
* The RESTCallBack object that implements the RequestCallback interface
*/
private class RESTRequestCallback implements RequestCallback
{
private RESTCallback m_origCallback;
public RESTRequestCallback(RESTCallback callback)
{
m_origCallback = callback;
}
/**
* Check the server response for an NCAC formatted fault and parse it into a RESTException
* if it is .
*
* @param val the JSON value returned from the server
* @param response the server response
*
* @return the RESTException if the server response contains an NCAC formatted fault
*/
private RESTException handleNcacFault(JSONValue val, Response response)
{
RESTCallStruct struct = RESTILITY.m_restCalls.get(m_origCallback);
int status = -1;
if (response != null) {
status = response.getStatusCode();
}
String url = null;
if (struct != null) {
url = struct.getUrl();
}
RESTException exception = JSONUtil.getRESTException(val, status, url);
if (exception == null) {
return null;
} else {
if (RESTException.AUTH_SERVER_UNAVAILABLE.equals(exception.getSubcode())) {
/*
* This is a special case where the server can't connect to the
* authentication server to validate the token.
*/
MessageUtil.showFatalError(STRINGS.unabledAuthServer());
}
return exception;
}
}
/**
* Handles an unauthorized (401) response from the server
*
* @param struct the struct for this request
* @param exception the exception for this request if available
* @param response the response object
*
* @return true if this is an invalid request and false otherwise
*/
private boolean handleUnauthorized(RESTCallStruct struct, RESTException exception, Response response)
{
if (response.getStatusCode() == Response.SC_UNAUTHORIZED) {
if (g_inLoginProcess) {
/*
* If we're already in the process of logging in then it will complete
* and this call will be replayed and we don't want to start a second
* login process.
*/
return true;
}
g_inLoginProcess = true;
m_logInListenerCalled = false;
/*
* For return values of 401 we need to show the login dialog
*/
try {
for (RESTLoginCallBack listener : g_loginListeners) {
listener.loginPrompt();
}
String code = null;
if (exception != null) {
code = exception.getSubcode();
}
doLogin(m_origCallback, response, struct.getUrl(), code, exception);
} catch (RESTException e) {
RESTILITY.m_restCalls.remove(m_origCallback);
m_origCallback.onError(e);
}
return true;
} else if (response.getStatusCode() == Response.SC_FORBIDDEN) {
handleNoPrivilege(exception);
return true;
} else {
return false;
}
}
@Override
public void onResponseReceived(Request request, Response response)
{
if (response.getStatusCode() == 0) {
/*
This means we couldn't contact the server. It might be that the
server is down or that we have a network timeout
*/
RESTCallStruct struct = RESTILITY.m_restCalls.remove(m_origCallback);
m_origCallback.onError(new RESTException(RESTException.NO_SERVER_RESPONSE,
"", STRINGS.noServerContact(),
new HashMap<String, String>(),
response.getStatusCode(),
struct.getUrl()));
return;
}
if (!checkJSON(response.getText())) {
if (handleUnauthorized(RESTILITY.m_restCalls.get(m_origCallback), null, response)) {
return;
} else {
RESTCallStruct struct = RESTILITY.m_restCalls.remove(m_origCallback);
m_origCallback.onError(new RESTException(RESTException.UNPARSABLE_RESPONSE,
"", "", new HashMap<String, String>(),
response.getStatusCode(),
struct.getUrl()));
return;
}
}
if (response.getStatusCode() > 399 &&
(response.getText() == null || response.getText().trim().length() == 0)) {
/*
* This is the case where the server returned an error response and didn't
* include any content. This is a bad thing to do, but we should handle this
* case and we can't call success here because that doesn't give the calling
* code access to the server response code. Instead we want to give them a
* special type of RESTException that they can handle which will give them
* access to the server response code and a simple path to handle this error.
*
* We only want to do this in the error case since many REST services will
* return a 200 or a 201 with no response content to indicate a simple success.
*/
RESTCallStruct struct = RESTILITY.m_restCalls.remove(m_origCallback);
m_origCallback.onError(new RESTException(RESTException.EMPTY_ERROR_RESPONSE,
"", "", new HashMap<String, String>(),
response.getStatusCode(),
struct.getUrl()));
return;
}
JSONValue val = null;
RESTException exception = null;
if (response.getText() != null &&
response.getText().trim().length() > 1) {
val = null;
try {
val = JSONParser.parseStrict(response.getText());
} catch (JavaScriptException e) {
/*
This means we couldn't parse the response this is unlikely
because we have already checked it, but it is possible.
*/
RESTCallStruct struct = RESTILITY.m_restCalls.get(m_origCallback);
exception = new RESTException(RESTException.UNPARSABLE_RESPONSE,
"", e.getMessage(), new HashMap<String, String>(),
response.getStatusCode(),
struct.getUrl());
}
exception = handleNcacFault(val, response);
}
RESTCallStruct struct = RESTILITY.m_restCalls.get(m_origCallback);
if (handleUnauthorized(struct, exception, response)) {
/*
Then this is a 401 and the login will handle it
*/
return;
} else {
handleSuccessfulResponse(val, exception, response);
}
}
/**
* Handle successful REST responses which have parsable JSON, aren't NCAC faults,
* and don't contain login requests.
*
* @param val the JSON value returned from the server
* @param exception the exception generated by the response if available
* @param response the server response
*/
private void handleSuccessfulResponse(JSONValue val, RESTException exception, Response response)
{
RESTILITY.m_restCalls.remove(m_origCallback);
if (exception != null) {
m_origCallback.onError(exception);
} else {
RESTILITY.m_callCount++;
/*
* You have to have at least three valid REST calls before
* we show the application UI. This covers the build info
* and the successful login with and invalid token. It
* would be really nice if we didn't have to worry about
* this at this level, but then the UI will flash sometimes
* before the user has logged in. Hackito Ergo Sum.
*/
if (RESTILITY.m_callCount > 2) {
fireLoginSuccess();
}
if (response.getHeader("ETag") != null &&
m_origCallback instanceof ConcurrentRESTCallback) {
((ConcurrentRESTCallback) m_origCallback).setETag(response.getHeader("ETag"));
}
m_origCallback.onSuccess(val);
}
}
public void onError(Request request, Throwable exception)
{
MessageUtil.showFatalError(exception.getMessage());
}
}
/**
* The GWT parser calls the JavaScript eval function. This is dangerous since it
* can execute arbitrary JavaScript. This method does a simple check to make sure
* the JSON data we get back from the server is safe to parse.
*
* This parsing scheme is taken from RFC 4627 - http://www.ietf.org/rfc/rfc4627.txt
*
* @param json the JSON string to test
*
* @return true if it is safe to parse and false otherwise
*/
private static native boolean checkJSON(String json) /*-{
return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
json.replace(/"(\\.|[^"\\])*"/g, '')));
}-*/;
/**
* Add login listeners
*
* @param callback listeners to be added
*/
public static void addLoginListener(RESTLoginCallBack callback)
{
RESTility.g_loginListeners.add(callback);
}
/**
* Remove login listeners
*
* @param callback listeners to be removed
*/
public static void removeLoginListener(RESTLoginCallBack callback)
{
RESTility.g_loginListeners.remove(callback);
}
}
/**
* A struct for holding data about a REST request
*/
class RESTCallStruct
{
private RESTOptions m_options;
/**
* Creates a new RESTCallStruct
*
* @param options the options for this REST call
*/
protected RESTCallStruct(RESTOptions options)
{
m_options = options;
}
/**
* Gets the options
*
* @return the options
*/
public RESTOptions getOptions()
{
return m_options;
}
/**
* Gets the URL
*
* @return the URL
*/
public String getUrl()
{
return m_options.getURL();
}
/**
* Gets the data
*
* @return the data
*/
public String getData()
{
return m_options.getDataString();
}
/**
* Gets the HTTP method
*
* @return the method
*/
public RESTility.HTTPMethod getMethod()
{
return m_options.getMethod();
}
/**
* Gets the ETag for this call
*
* @return the ETag
*/
public String getETag()
{
return m_options.getEtag();
}
/**
* Should this request repeat
*
* @return true if it should repeat, false otherwise
*/
public boolean shouldReplay()
{
return m_options.shouldReplay();
}
}
/**
* This class extends RequestBuilder so we can call PUT and DELETE
*/
class RESTRequestBuilder extends RequestBuilder
{
/**
* Creates a new RESTRequestBuilder
*
* @param method the HTTP method
* @param url the request URL
*/
public RESTRequestBuilder(String method, String url)
{
super(method, url);
}
}