HttpUtil.java

/***************************************************************************
   Copyright 2009 Emily Estes

   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 net.metanotion.web.concrete;


import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.metanotion.io.Serializer;
import net.metanotion.util.Pair;
import net.metanotion.util.Wrap;
import net.metanotion.web.Cookie;
import net.metanotion.web.HttpStatus;
import net.metanotion.web.HttpValues;

/** Utilty methods for generating common types of HTTP responses.
This class is just a collection of utility methods for generating many common types of HTTP responses.
*/
public final class HttpUtil {
	private static final String HTML_MIME = "text/html; charset=utf-8";
	private static final String JSON_MIME = "application/json; charset=utf-8";
	private static final String CONTENT_TYPE = "Content-Type";
	private static final String LOCATION = "Location";
	/** Many of the SimpleHttpValues instances use an empty list. This is just a short cut instead of making a new one
	every time. */
	private static final List<Map.Entry<String,Object>> EMPTY_LIST = (List<Map.Entry<String,Object>>) Collections.EMPTY_LIST;
	/** Many common responses just need to send a header list with a Content-Type set to UTF-8 encoded HTML. */
	private static final List<Map.Entry<String,Object>> BASIC_HEADER_LIST =
		Collections.unmodifiableList(Arrays.asList((Map.Entry<String,Object>) new Pair<String,Object>(CONTENT_TYPE, HTML_MIME)));
	/** All the basic JSON responses need the Content-Type set to JSON. */
	private static final List<Map.Entry<String,Object>> JSON_HEADER_LIST =
		Collections.unmodifiableList(Arrays.asList((Map.Entry<String,Object>) new Pair<String,Object>(CONTENT_TYPE, JSON_MIME)));
	private static final String EMPTY_STRING = "";

	/** Generate an HTTP response that simply returns an HTTP Status code with blank content.
		@param status The HTTP status code to return.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues newStatus(final int status) {
		return new SimpleHttpValues(HttpUtil.BASIC_HEADER_LIST, HttpUtil.EMPTY_LIST, HttpUtil.EMPTY_STRING, status);
	}

	/** Issue temporary redirect(303) to another url.
		@param url The URL to redirect the request to.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues newRedirect(final String url) {
		return new SimpleHttpValues(Arrays.asList((Map.Entry<String,Object>) new Pair<String,Object>(LOCATION, url)),
										HttpUtil.EMPTY_LIST,
										HttpUtil.EMPTY_STRING,
										HttpStatus.REDIRECT_SEE_OTHER.codeNumber());
	}

	/** Issue a redirect by code number(actually, this just sets the Location header and uses whatever code number you
		give it, but it should be one of the 3xx codes to actually work correctly.
		@param url The URL to redirect the request to.
		@param type The HTTP status code of the response.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues newRedirect(final String url, final HttpStatus type) {
		return new SimpleHttpValues(Arrays.asList((Map.Entry<String,Object>) new Pair<String,Object>(LOCATION, url)),
										HttpUtil.EMPTY_LIST,
										HttpUtil.EMPTY_STRING,
										type.codeNumber());
	}

	/** Generate a 404 Not found response.
		@param message The content(presumed to be HTML) to send with the response.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues new404(final String message) {
		return new SimpleHttpValues(HttpUtil.BASIC_HEADER_LIST, HttpUtil.EMPTY_LIST, message, HttpStatus.NOT_FOUND.codeNumber());
	}

	/** Generate a 403 Forbidden response.
		@param message The content(presumed to be HTML) to send with the response.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues new403(final String message) {
		return new SimpleHttpValues(HttpUtil.BASIC_HEADER_LIST, HttpUtil.EMPTY_LIST, message, HttpStatus.FORBIDDEN.codeNumber());
	}

	/** Generate a 500 internal server error response.
		@param message The content(presumed to be HTML) to send with the response.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues new500(final String message) {
		return new SimpleHttpValues(HttpUtil.BASIC_HEADER_LIST,
			HttpUtil.EMPTY_LIST,
			message,
			HttpStatus.SERVER_ERROR.codeNumber());
	}

	/** Send back an HTML response. This is a "normal" response with a status of 200 "OK".
		@param html The content, presumed to be HTML.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues newHtmlString(final String html) {
		return new SimpleHttpValues(HttpUtil.BASIC_HEADER_LIST, HttpUtil.EMPTY_LIST, html);
	}

	/** Send a basic response to the client with a custom MIME type and a status of 200 "OK".
		@param content A object encoding a representation of the content suitable for issuing as a reply(as a fallback,
			{@link java.lang.Object#toString} is used to generate a representation of the content to send to the
			client).
		@param contentType The MIME-type to set for the value of the Content-Type header.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues newTypedContent(final Object content, final String contentType) {
		return new SimpleHttpValues(
			Arrays.asList((Map.Entry<String,Object>) new Pair<String,Object>(CONTENT_TYPE, contentType)),
			HttpUtil.EMPTY_LIST,
			content);
	}

	/** Generate a JSON response, and send it with 200 OK status.
		@param content A JSON object or an encoding of a JSON object suitable for issuing as a reply(as a fallback,
			{@link java.lang.Object#toString} is used to generate a representation of the content to send to the
			client).
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues newJsonResponse(final Object content) {
		return newJsonResponse(content, HttpStatus.OK.codeNumber());
	}

	/** Generate a JSON response with a custom status.
		@param content A JSON object or an encoding of a JSON object suitable for issuing as a reply(as a fallback,
			{@link java.lang.Object#toString} is used to generate a representation of the content to send to the
			client).
		@param type The HTTP status code of the response.
		@return An HttpValues instance encoding the response.
	*/
	public static HttpValues newJsonResponse(final Object content, final int type) {
		return new SimpleHttpValues(HttpUtil.JSON_HEADER_LIST, HttpUtil.EMPTY_LIST, content, type);
	}

	/** Convert an HTTP response object into a printable string for debugging purposes. This uses the same algorithm as
	the framework does in {@link net.metanotion.web.servlets.RequestHandler}
		@param result The HTTP response to serialize.
		@return a string representing the HTTP response.
		@throws IOException if there is a problem serializing the response.
	*/
	public static String debugString(final Object result) throws IOException {
		final StringBuilder sb = new StringBuilder("");
		if(result == null) { return "NULL"; }
		if(result instanceof HttpValues) {
			final HttpValues hv = (HttpValues) result;
			sb.append("HTTP " + Integer.toString(hv.getHttpStatus()) + "\n");
			for(final Iterator<Map.Entry<String,Object>> it = hv.listHeaders(); it.hasNext();) {
				final Map.Entry<String,Object> e = it.next();
				sb.append("HEADER: " + e.getKey() + " = " + e.getValue().toString() + "\n");
			}
			for(final Iterator<Map.Entry<String,Object>> it = hv.listCookies(); it.hasNext();) {
				final Map.Entry<String,Object> e = it.next();
				sb.append("COOKIE: " + e.getKey() + " = '");
				final Object val = e.getValue();
				sb.append(((val instanceof Cookie) ? ((Cookie) val).getValue() : val.toString()) + "'\n");
			}
		}
		final Object content = (result instanceof Wrap) ? ((Wrap) result).unwrap() : result;
		final ByteArrayOutputStream baos = new ByteArrayOutputStream();
		Serializer.write(content, baos, StandardCharsets.UTF_8);
		return sb.append(new String(baos.toByteArray(), StandardCharsets.UTF_8)).toString();
	}
}