/*******************************************************************************
* Copyright (c) 2013, 2014 Lectorius, Inc.
* Authors:
* Vijay Pandurangan (vijayp@mitro.co)
* Evan Jones (ej@mitro.co)
* Adam Hilss (ahilss@mitro.co)
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the authors at inbound@mitro.co.
*******************************************************************************/
package co.mitro.core.servlets;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletResponse;
import com.google.gson.Gson;
public final class Util {
// not constructible
private Util() {}
// http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#e-mail-state-(type=email)
private static final Pattern VALID_EMAIL = Pattern.compile(
"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$");
private static final Gson gson = new Gson();
/** Content-Type header for JSON with charset=UTF-8 so Jetty serializes Unicode correctly. */
public static final String JSON_CONTENT_TYPE = "application/json; charset=UTF-8";
public static final String CORS_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
public static final String CORS_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
public static final String CORS_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
public static final String CORS_MAX_AGE_HEADER = "Access-Control-Max-Age";
/** Returns true if address looks like a valid email address. Uses the HTML5 regex. */
public static boolean isEmailAddress(String address) {
Matcher matcher = VALID_EMAIL.matcher(address);
return matcher.matches();
}
// TODO: Use this throughout Mitro!
/** Returns the "normalized" version of address. Currently this just lowercases the address. */
public static String normalizeEmailAddress(String address) {
// TODO: Is this a sane policy? Strictly speaking this is a violation of the RFCs
assert isEmailAddress(address);
return address.toLowerCase();
}
/**
* Writes gsonValue as JSON to response, serializing it using Gson. This sets the content type
* correctly, so Unicode characters get serialized in the right format.
*/
public static void writeJsonResponse(Object gsonValue, HttpServletResponse response)
throws IOException {
// By default, JSON should be interpreted as UTF-8, but servlets default to ISO-8859-1:
// http://wiki.apache.org/tomcat/FAQ/CharacterEncoding#Q1
response.setContentType(JSON_CONTENT_TYPE);
response.getWriter().write(gson.toJson(gsonValue));
}
/**
* Sets response headers to permit cross-origin requests. See:
* http://www.w3.org/TR/cors/#access-control-allow-origin-response-header
*/
public static void allowCrossOriginRequests(HttpServletResponse response) {
// must be *: Accessed by both mitroweb.com and by the extension
// Firefox: GET ServerRejects failing due to Origin: resource://mitro-login-manager-at-jetpack
// POST requests are sent without Origin, possibly because they are from a "window" context?
// https://developer.mozilla.org/en-US/Add-ons/Extension_Frequently_Asked_Questions#I_cannot_initiate_an_XMLHttpRequest_from_my_extension
// To be safe: permit all origins
response.setHeader(CORS_ALLOW_ORIGIN_HEADER, "*");
response.setHeader(CORS_ALLOW_METHODS_HEADER, "POST, GET, OPTIONS");
response.setHeader(CORS_ALLOW_HEADERS_HEADER, "*");
response.setHeader(CORS_MAX_AGE_HEADER, "3600");
}
// Url encode a set of key=value params.
// For example the input params {a: b, c: d&e} would be encoded as a=b&c=d%26e.
public static String urlEncode(Map<String, String> params) {
assert(params != null);
StringBuilder paramsBuilder = new StringBuilder();
boolean firstParam = true;
for (Map.Entry<String, String> entry : params.entrySet()) {
if (firstParam) {
firstParam = false;
} else {
paramsBuilder.append("&");
}
try {
paramsBuilder.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
paramsBuilder.append("=");
paramsBuilder.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Should never fail: : UTF-8 is required to be supported by the JVM.
throw new RuntimeException(e);
}
}
return paramsBuilder.toString();
}
// Build a url from host, path, and query params.
// Host includes the scheme and optional port e.g. http://example.com:8000.
// Query params is optional (pass params=null).
public static String buildUrl(String host, String path, Map<String, String> params) {
assert(host != null);
assert(path != null);
StringBuilder url = new StringBuilder();
url.append(host);
url.append(path);
if (params != null && !params.isEmpty()) {
url.append("?");
url.append(urlEncode(params));
}
return url.toString();
}
}