All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.officefloor.server.http.impl.ProcessAwareHttpResponse Maven / Gradle / Ivy

/*-
 * #%L
 * HTTP Server
 * %%
 * Copyright (C) 2005 - 2020 Daniel Sagenschneider
 * %%
 * 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.
 * #L%
 */

package net.officefloor.server.http.impl;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.concurrent.RejectedExecutionException;

import net.officefloor.frame.api.managedobject.ManagedObjectContext;
import net.officefloor.frame.api.managedobject.ProcessSafeOperation;
import net.officefloor.frame.api.managedobject.recycle.CleanupEscalation;
import net.officefloor.frame.api.team.TeamOverloadException;
import net.officefloor.server.http.CleanupException;
import net.officefloor.server.http.DateHttpHeaderClock;
import net.officefloor.server.http.HttpEscalationContext;
import net.officefloor.server.http.HttpEscalationHandler;
import net.officefloor.server.http.HttpException;
import net.officefloor.server.http.HttpHeader;
import net.officefloor.server.http.HttpHeaderName;
import net.officefloor.server.http.HttpHeaderValue;
import net.officefloor.server.http.HttpResponse;
import net.officefloor.server.http.HttpResponseCookies;
import net.officefloor.server.http.HttpResponseHeaders;
import net.officefloor.server.http.HttpResponseWriter;
import net.officefloor.server.http.HttpStatus;
import net.officefloor.server.http.HttpVersion;
import net.officefloor.server.http.ServerHttpConnection;
import net.officefloor.server.http.WritableHttpHeader;
import net.officefloor.server.stream.ServerOutputStream;
import net.officefloor.server.stream.ServerWriter;
import net.officefloor.server.stream.impl.BufferPoolServerOutputStream;
import net.officefloor.server.stream.impl.CloseHandler;
import net.officefloor.server.stream.impl.ProcessAwareServerOutputStream;
import net.officefloor.server.stream.impl.ProcessAwareServerWriter;

/**
 * {@link Serializable} {@link HttpResponse}.
 * 
 * @author Daniel Sagenschneider
 */
public class ProcessAwareHttpResponse implements HttpResponse, CloseHandler {

	/**
	 * Server {@link HttpHeaderName}.
	 */
	private static final HttpHeaderName SERVER_HTTP_HEADER_NAME = new HttpHeaderName("Server");

	/**
	 * Date {@link HttpHeaderName}.
	 */
	private static final HttpHeaderName DATE_HTTP_HEADER_NAME = new HttpHeaderName("Date");

	/**
	 * Binary Content-Type.
	 */
	private static final HttpHeaderValue BINARY_CONTENT = new HttpHeaderValue("application/octet-stream");

	/**
	 * Text Content-Type.
	 */
	private static final HttpHeaderValue TEXT_CONTENT = new HttpHeaderValue("text/plain");

	/**
	 * {@link ProcessAwareServerHttpConnectionManagedObject}.
	 */
	private final ProcessAwareServerHttpConnectionManagedObject serverHttpConnection;

	/**
	 * Client {@link HttpVersion}.
	 */
	private final HttpVersion clientVersion;

	/**
	 * {@link HttpVersion}.
	 */
	private HttpVersion version;

	/**
	 * {@link HttpStatus}.
	 */
	private HttpStatus status = HttpStatus.OK;

	/**
	 * {@link ProcessAwareHttpResponseHeaders}.
	 */
	private ProcessAwareHttpResponseHeaders headers;

	/**
	 * {@link ProcessAwareHttpResponseCookies}.
	 */
	private ProcessAwareHttpResponseCookies cookies;

	/**
	 * {@link BufferPoolServerOutputStream}.
	 */
	private final BufferPoolServerOutputStream bufferPoolOutputStream;

	/**
	 * {@link ProcessAwareServerOutputStream}.
	 */
	private final ProcessAwareServerOutputStream safeOutputStream;

	/**
	 * {@link ServerWriter}.
	 */
	private ServerWriter entityWriter = null;

	/**
	 * {@link ManagedObjectContext}.
	 */
	private final ManagedObjectContext managedObjectContext;

	/**
	 * {@link CleanupEscalation} instances. Will be null if successful
	 * clean up.
	 */
	private CleanupEscalation[] cleanupEscalations = null;

	/**
	 * {@link HttpEscalationHandler}.
	 */
	private HttpEscalationHandler escalationHandler = null;

	/**
	 * Content-Type value.
	 */
	private HttpHeaderValue contentType = null;

	/**
	 * {@link Charset} for the {@link ServerWriter}.
	 */
	private Charset charset = ServerHttpConnection.DEFAULT_HTTP_ENTITY_CHARSET;

	/**
	 * Indicates if this {@link HttpResponse} has been sent.
	 */
	private boolean isSent = false;

	/**
	 * Indicates if this {@link HttpResponse} has been written to the
	 * {@link HttpResponseWriter}.
	 */
	private boolean isWritten = false;

	/**
	 * Instantiate.
	 * 
	 * @param serverHttpConnection {@link ServerHttpConnection}.
	 * @param version              {@link HttpVersion}.
	 * @param managedObjectContext {@link ManagedObjectContext}.
	 */
	public ProcessAwareHttpResponse(ProcessAwareServerHttpConnectionManagedObject serverHttpConnection,
			HttpVersion version, ManagedObjectContext managedObjectContext) {
		this.serverHttpConnection = serverHttpConnection;
		this.clientVersion = version;
		this.version = version;
		this.headers = new ProcessAwareHttpResponseHeaders(managedObjectContext);
		this.cookies = new ProcessAwareHttpResponseCookies(managedObjectContext);
		this.bufferPoolOutputStream = new BufferPoolServerOutputStream<>(this.serverHttpConnection.bufferPool, this);
		this.safeOutputStream = new ProcessAwareServerOutputStream(this.bufferPoolOutputStream, managedObjectContext);
		this.managedObjectContext = managedObjectContext;
	}

	/**
	 * Flushes the {@link HttpResponse} to the {@link HttpResponseWriter}.
	 * 
	 * @param escalation Possible escalation in servicing. Will be null
	 *                   if successful.
	 * @throws IOException If fails to flush {@link HttpResponse} to the
	 *                     {@link HttpResponseWriter}.
	 */
	public void flushResponseToHttpResponseWriter(Throwable escalation) throws IOException {
		this.safe(() -> {
			this.unsafeFlushResponseToHttpResponseWriter(escalation);
			return null;
		});
	}

	/**
	 * Sets the {@link CleanupEscalation} instances.
	 * 
	 * @param cleanupEscalations {@link CleanupEscalation} instances.
	 * @throws IOException If fails to send {@link CleanupEscalation} details for
	 *                     this {@link HttpResponse}.
	 */
	void setCleanupEscalations(CleanupEscalation[] cleanupEscalations) throws IOException {

		// Determine if have escalations
		if (cleanupEscalations.length == 0) {
			return; // no escalations
		}

		// Store the clean up escalations
		this.managedObjectContext.run(() -> {
			this.cleanupEscalations = cleanupEscalations;
			return null;
		});
	}

	/**
	 * Flushes this {@link HttpResponse} to the {@link HttpResponseWriter}.
	 * 
	 * @param escalation Possible escalation in servicing. Will be null
	 *                   if successful.
	 * @throws IOException If fails to flush {@link HttpResponse} to the
	 *                     {@link HttpResponseWriter}.
	 */
	private void unsafeFlushResponseToHttpResponseWriter(Throwable escalation) throws IOException {

		// Determine if already written
		if (this.isWritten) {
			return; // already written
		}

		// Determine if clean up escalation
		if ((escalation == null) && (this.cleanupEscalations != null)) {
			// No escalation, but clean up escalation
			escalation = new CleanupException(this.cleanupEscalations);
		}

		// Handle escalation
		if (escalation != null) {

			// Clear sent to allow writing content
			this.isSent = false;

			// Reset the response to send an error
			this.unsafeReset();

			// Provide status and headers
			this.loadErrorStatusAndHeaders(escalation);

			// Determine if escalation handler
			boolean isHandled = false;
			if (this.escalationHandler != null) {

				// Handle the escalation
				try {
					final Throwable finalEscalation = escalation;
					isHandled = this.escalationHandler.handle(new HttpEscalationContext() {

						@Override
						public Throwable getEscalation() {
							return finalEscalation;
						}

						@Override
						public boolean isIncludeStacktrace() {
							return ProcessAwareHttpResponse.this.serverHttpConnection.isIncludeStackTraceOnEscalation;
						}

						@Override
						public ServerHttpConnection getServerHttpConnection() {
							return ProcessAwareHttpResponse.this.serverHttpConnection;
						}
					});
				} catch (Throwable internalServerError) {

					// Send failure to send response
					escalation = internalServerError;

					// Reset the response to send generic error
					this.unsafeReset();

					// Provide status and headers
					this.loadErrorStatusAndHeaders(escalation);
				}
			}

			// If not handled, handle generically
			if (!isHandled) {

				// Default send the escalation
				if (escalation instanceof HttpException) {
					// Send the HTTP entity (if provided)
					HttpException httpEscalation = (HttpException) escalation;
					String entity = httpEscalation.getEntity();
					if (entity != null) {
						PrintWriter writer = new PrintWriter(this.bufferPoolOutputStream
								.getServerWriter(ServerHttpConnection.DEFAULT_HTTP_ENTITY_CHARSET));
						writer.write(entity);
						writer.flush();
					}

				} else if (this.serverHttpConnection.isIncludeStackTraceOnEscalation) {
					// Send escalation stack trace
					this.contentType = TEXT_CONTENT;
					PrintWriter writer = new PrintWriter(this.bufferPoolOutputStream
							.getServerWriter(ServerHttpConnection.DEFAULT_HTTP_ENTITY_CHARSET));
					escalation.printStackTrace(writer);
					writer.flush();
				}
			}
		}

		// Ensure send
		this.unsafeSend();

		// Determine content details
		long contentLength = this.bufferPoolOutputStream.getContentLength();
		HttpHeaderValue contentType = null;
		if (contentLength > 0) {
			// Have content so derive content type
			contentType = this.deriveContentType();
		} else if (this.status == HttpStatus.OK) {
			// No content, so update to no content
			this.status = HttpStatus.NO_CONTENT;
		}

		// Obtain the headers
		WritableHttpHeader httpHeaders = this.headers.getWritableHttpHeaders();

		// Date HTTP header (if specified)
		DateHttpHeaderClock clock = this.serverHttpConnection.dateHttpHeaderClock;
		if (clock != null) {
			HttpHeaderValue dateValue = clock.getDateHttpHeaderValue();
			WritableHttpHeader dateHeader = new WritableHttpHeader(DATE_HTTP_HEADER_NAME, dateValue);
			dateHeader.next = httpHeaders;
			httpHeaders = dateHeader;
		}

		// Server HTTP header (if specified)
		HttpHeaderValue serverHttpHeaderValue = this.serverHttpConnection.serverName;
		if (serverHttpHeaderValue != null) {
			WritableHttpHeader serverHeader = new WritableHttpHeader(SERVER_HTTP_HEADER_NAME, serverHttpHeaderValue);
			serverHeader.next = httpHeaders;
			httpHeaders = serverHeader;
		}

		// Write the response (and consider written)
		this.isWritten = true;
		this.serverHttpConnection.httpResponseWriter.writeHttpResponse(this.version, this.status, httpHeaders,
				this.cookies.getWritableHttpCookie(), contentLength, contentType,
				this.bufferPoolOutputStream.getBuffers());
	}

	/**
	 * Loads the error status and headers.
	 * 
	 * @param escalation {@link Throwable}.
	 */
	private void loadErrorStatusAndHeaders(Throwable escalation) {
		try {
			throw escalation;

		} catch (HttpException httpEscalation) {
			// Provide the HTTP escalation
			this.status = httpEscalation.getHttpStatus();
			HttpHeader[] escalationHeaders = httpEscalation.getHttpHeaders();
			for (int i = 0; i < escalationHeaders.length; i++) {
				HttpHeader escalationHeader = escalationHeaders[i];
				this.headers.addHeader(escalationHeader.getName(), escalationHeader.getValue());
			}

		} catch (RejectedExecutionException | TeamOverloadException overloaded) {
			// Server overloaded
			this.status = HttpStatus.SERVICE_UNAVAILABLE;

		} catch (Throwable internalServerError) {
			// Unknown escalation, so error
			this.status = HttpStatus.INTERNAL_SERVER_ERROR;
		}
	}

	/**
	 * Unsafe send {@link HttpResponse}.
	 * 
	 * @throws IOException If fails to send.
	 */
	private void unsafeSend() throws IOException {

		// Determine if already sent
		if (this.isSent) {
			return; // already sent
		}

		// If writer, ensure flushed
		if (this.entityWriter != null) {
			this.entityWriter.flush();
		}

		// Consider sent
		this.isSent = true;
	}

	/**
	 * Unsafe reset {@link HttpResponse}.
	 * 
	 * @throws IOException If fails to reset.
	 */
	private void unsafeReset() throws IOException {

		// Reset the response
		this.version = this.clientVersion;
		this.status = HttpStatus.OK;
		this.headers = new ProcessAwareHttpResponseHeaders(this.managedObjectContext);
		this.cookies = new ProcessAwareHttpResponseCookies(this.managedObjectContext);

		// Release writing content
		this.contentType = null;
		this.charset = ServerHttpConnection.DEFAULT_HTTP_ENTITY_CHARSET;
		this.entityWriter = null;

		// Release the buffers
		if (this.entityWriter != null) {
			this.entityWriter.flush(); // clear content
		}
		this.bufferPoolOutputStream.clear();
	}

	/**
	 * Derives the Content-Type.
	 * 
	 * @return Content-Type
	 */
	private HttpHeaderValue deriveContentType() {

		// Determine if content-type specified
		if (this.contentType != null) {
			return this.contentType;
		} else {
			// Not specified, so determine based on state
			return (this.entityWriter == null ? BINARY_CONTENT : TEXT_CONTENT);
		}
	}

	/**
	 * Determines if can change the Content-Type and {@link Charset}.
	 * 
	 * @throws IOException If not able to change.
	 */
	private void allowContentTypeChange() throws IOException {
		if (this.entityWriter != null) {
			throw new IOException("Can not change Content-Type. Committed to writing "
					+ this.deriveContentType().getValue() + " (charset " + this.charset.name() + ")");
		}
	}

	/**
	 * Undertakes a {@link ProcessSafeOperation}.
	 * 
	 * @param operation {@link ProcessSafeOperation}.
	 * @return Result of {@link ProcessSafeOperation}.
	 * @throws T Possible {@link Throwable} from {@link ProcessSafeOperation}.
	 */
	private  R safe(ProcessSafeOperation operation) throws T {
		return this.managedObjectContext.run(operation);
	}

	/*
	 * ===================== HttpResponse ========================
	 */

	@Override
	public HttpVersion getVersion() {
		return this.safe(() -> this.version);
	}

	@Override
	public void setVersion(HttpVersion version) {
		if (version == null) {
			throw new IllegalArgumentException("Must provide version");
		}
		this.safe(() -> this.version = version);
	}

	@Override
	public HttpStatus getStatus() {
		return this.safe(() -> this.status);
	}

	@Override
	public void setStatus(HttpStatus status) {
		if (status == null) {
			throw new IllegalArgumentException("Must provide status");
		}
		this.safe(() -> this.status = status);
	}

	@Override
	public HttpResponseHeaders getHeaders() {
		return this.headers;
	}

	@Override
	public HttpResponseCookies getCookies() {
		return this.cookies;
	}

	@Override
	public void setContentType(String contentType, Charset charset) throws IOException {
		this.safe(() -> {

			// Ensure can change Content-Type
			this.allowContentTypeChange();

			// Specify the charset (ensuring default)
			this.charset = (charset != null ? charset : ServerHttpConnection.DEFAULT_HTTP_ENTITY_CHARSET);

			// Specify the content type
			String nonNullContentType = (contentType != null ? contentType : TEXT_CONTENT.getValue());
			if (this.charset == ServerHttpConnection.DEFAULT_HTTP_ENTITY_CHARSET) {
				this.contentType = new HttpHeaderValue(contentType);
			} else {
				// Ensure have content type
				this.contentType = new HttpHeaderValue(nonNullContentType + "; charset=" + this.charset.name());
			}

			// Void return
			return null;
		});
	}

	@Override
	public void setContentType(HttpHeaderValue contentTypeAndCharsetValue, Charset charset) throws IOException {
		this.safe(() -> {

			// Ensure can change Content-Type
			this.allowContentTypeChange();

			// Specify the charset (ensuring default)
			this.charset = (charset != null ? charset : ServerHttpConnection.DEFAULT_HTTP_ENTITY_CHARSET);

			// Use content-type as is
			this.contentType = contentTypeAndCharsetValue;

			// Void return
			return null;
		});
	}

	@Override
	public String getContentType() {
		return this.safe(() -> this.deriveContentType().getValue());
	}

	@Override
	public Charset getContentCharset() {
		return this.safe(() -> this.charset);
	}

	@Override
	public ServerOutputStream getEntity() throws IOException {
		return this.safeOutputStream;
	}

	@Override
	public ServerWriter getEntityWriter() throws IOException {
		return this.safe(() -> {
			if (this.entityWriter == null) {
				this.entityWriter = new ProcessAwareServerWriter(
						this.bufferPoolOutputStream.getServerWriter(this.charset), this.managedObjectContext);
			}
			return this.entityWriter;
		});
	}

	@Override
	public HttpEscalationHandler getEscalationHandler() {
		return this.safe(() -> this.escalationHandler);
	}

	@Override
	public void setEscalationHandler(HttpEscalationHandler escalationHandler) {
		this.safe(() -> {
			this.escalationHandler = escalationHandler;
			return null;
		});
	}

	@Override
	public void reset() throws IOException {
		this.safe(() -> {

			// Ensure not written
			if (this.isSent) {
				throw new IOException("Already committed to send response");
			}

			// Reset the response
			this.unsafeReset();

			// Void return
			return null;
		});
	}

	@Override
	public void send() throws IOException {
		this.safe(() -> {

			// Send
			this.unsafeSend();

			// Void return
			return null;
		});
	}

	/*
	 * ======================= CloseHandler =================================
	 */

	@Override
	public boolean isClosed() {

		// Closed if sent
		return this.isSent;
	}

	@Override
	public void close() throws IOException {

		// Send on close
		this.send();
	}

}