
ro.pippo.core.Response Maven / Gradle / Ivy
/*
* Copyright (C) 2014 the original author or authors.
*
* 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 ro.pippo.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.pippo.core.route.RouteContext;
import ro.pippo.core.route.RouteDispatcher;
import ro.pippo.core.util.DateUtils;
import ro.pippo.core.util.IoUtils;
import ro.pippo.core.util.MimeTypes;
import ro.pippo.core.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author Decebal Suiu
*/
public final class Response {
private static final Logger log = LoggerFactory.getLogger(Response.class);
private HttpServletResponse httpServletResponse;
private ContentTypeEngines contentTypeEngines;
private TemplateEngine templateEngine;
private Map locals;
private Map headers;
private Map cookies;
private String contextPath;
private String applicationPath;
private ResponseFinalizeListenerList finalizeListeners;
private MimeTypes mimeTypes;
private int status;
public Response(HttpServletResponse httpServletResponse, Application application) {
this.httpServletResponse = httpServletResponse;
this.contentTypeEngines = application.getContentTypeEngines();
this.templateEngine = application.getTemplateEngine();
this.httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());
this.contextPath = application.getRouter().getContextPath();
this.applicationPath = StringUtils.removeEnd(application.getRouter().getApplicationPath(), "/");
this.mimeTypes = application.getMimeTypes();
this.status = 0;
}
/**
* Map of bound objects which can be stored and shared between all handlers
* for the current request/response cycle.
*
* All bound objects are made available to the template engine during parsing.
*
*
* @return the bound objects map
*/
public Map getLocals() {
if (locals == null) {
locals = new HashMap<>();
}
return locals;
}
/**
* Shortcut for getLocals().get("flash")
.
* @return
*/
public Flash getFlash() {
return (Flash) getLocals().get("flash");
}
/**
* Binds an object to the response.
*
* @param name
* @param model
* @return the response
*/
public Response bind(String name, Object model) {
getLocals().put(name, model);
return this;
}
/**
* Returns the servlet response.
*
* @return the servlet response
*/
public HttpServletResponse getHttpServletResponse() {
return httpServletResponse;
}
/**
* Gets the character encoding of the response.
*
* @return the character encoding
*/
public String getCharacterEncoding() {
return getHttpServletResponse().getCharacterEncoding();
}
/**
* Sets the character encoding of the response.
*
* @param charset
* @return the response
*/
public Response characterEncoding(String charset) {
checkCommitted();
getHttpServletResponse().setCharacterEncoding(charset);
return this;
}
private void addCookie(Cookie cookie) {
checkCommitted();
if (StringUtils.isNullOrEmpty(cookie.getPath())) {
cookie.setPath(StringUtils.addStart(contextPath, "/"));
}
getCookieMap().put(cookie.getName(), cookie);
}
/**
* Adds a cookie to the response.
*
* @param cookie
* @return the response
*/
public Response cookie(Cookie cookie) {
addCookie(cookie);
return this;
}
/**
* Adds a cookie to the response.
*
* @param name
* @param value
* @return the response
*/
public Response cookie(String name, String value) {
Cookie cookie = new Cookie(name, value);
addCookie(cookie);
return this;
}
/**
* Adds a cookie to the response.
*
* @param name
* @param value
* @param maxAge
* @return the response
*/
public Response cookie(String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setMaxAge(maxAge);
addCookie(cookie);
return this;
}
/**
* Adds a cookie to the response.
*
* @param path
* @param domain
* @param name
* @param value
* @param maxAge
* @param secure
* @return the response
*/
public Response cookie(String path, String domain, String name, String value, int maxAge, boolean secure) {
Cookie cookie = new Cookie(name, value);
cookie.setPath(path);
cookie.setDomain(domain);
cookie.setMaxAge(maxAge);
cookie.setSecure(secure);
addCookie(cookie);
return this;
}
/**
* Returns all cookies added to the response.
*
* @return the cookies added to the response
*/
public Collection getCookies() {
return getCookieMap().values();
}
/**
* Gets the specified cookie by name.
*
* @param name
* @return the cookie or null
*/
public Cookie getCookie(String name) {
return getCookieMap().get(name);
}
/**
* Removes the specified cookie by name.
*
* @param name
* @return the response
*/
public Response removeCookie(String name) {
Cookie cookie = new Cookie(name, "");
cookie.setMaxAge(0);
addCookie(cookie);
return this;
}
private Map getCookieMap() {
if (cookies == null) {
cookies = new HashMap<>();
}
return cookies;
}
private boolean isHeaderEmpty(String name) {
return StringUtils.isNullOrEmpty(getHeaderMap().get(name));
}
/**
* Sets a header.
*
* @param name
* @param value
* @return the response
*/
public Response header(String name, String value) {
checkCommitted();
getHeaderMap().put(name, value);
return this;
}
/**
* Sets a header.
*
* @param name
* @param value
* @return the response
*/
public Response header(String name, Date value) {
checkCommitted();
getHeaderMap().put(name, DateUtils.formatForHttpHeader(value));
return this;
}
/**
* Returns a header value, if set in the Response.
*
* @param name
* @return the header value or null
*/
public String getHeader(String name) {
return getHeaderMap().get(name);
}
private Map getHeaderMap() {
if (headers == null) {
headers = new HashMap<>();
}
return headers;
}
/**
* Sets this response as not cacheable.
*
* @return the response
*/
public Response noCache() {
checkCommitted();
// no-cache headers for HTTP/1.1
header(HttpConstants.Header.CACHE_CONTROL, "no-store, no-cache, must-revalidate");
// no-cache headers for HTTP/1.1 (IE)
header(HttpConstants.Header.CACHE_CONTROL, "post-check=0, pre-check=0");
// no-cache headers for HTTP/1.0
header(HttpConstants.Header.PRAGMA, "no-cache");
// set the expires to past
httpServletResponse.setDateHeader("Expires", 0);
return this;
}
/**
* Gets the status code of the response.
*
* @return the status code
*/
public int getStatus() {
return status;
}
/**
* Sets the status code of the response.
*
* @param status
* @return the response
*/
public Response status(int status) {
checkCommitted();
httpServletResponse.setStatus(status);
this.status = status;
return this;
}
/**
* Redirect the browser to a location which may be...
*
* - relative to the current request
*
- relative to the servlet container root (if location starts with '/')
*
- an absolute url
*
* If you want a context-relative redirect, use the
* {@link ro.pippo.core.Response#redirectToContextPath(String)} method.
* If you want an application-relative redirect, use the
* {@link ro.pippo.core.Response#redirectToApplicationPath(String)} method.
* This method commits the response.
*
* @param location
* Where to redirect
*/
public void redirect(String location) {
checkCommitted();
finalizeResponse();
try {
httpServletResponse.sendRedirect(location);
} catch (IOException e) {
throw new PippoRuntimeException(e);
}
}
/**
* Redirects the browser to a path relative to the application context. For
* example, redirectToContextPath("/contacts") might redirect the browser to
* http://localhost/myContext/contacts
* This method commits the response.
*
* @param path
*/
public void redirectToContextPath(String path) {
if ("".equals(contextPath)) {
// context path is the root
redirect(path);
} else {
redirect(contextPath + StringUtils.addStart(path, "/"));
}
}
/**
* Redirects the browser to a path relative to the Pippo application root. For
* example, redirectToApplicationPath("/contacts") might redirect the browser to
* http://localhost/myContext/myApp/contacts
* This method commits the response.
*
* @param path
*/
public void redirectToApplicationPath(String path) {
if ("".equals(applicationPath)) {
// application path is the root
redirect(path);
} else {
redirect(applicationPath + StringUtils.addStart(path, "/"));
}
}
/**
* A permanent (3XX status code) redirect.
* This method commits the response.
*
* @param location
* @param statusCode
*/
public void redirect(String location, int statusCode) {
checkCommitted();
finalizeResponse();
status(statusCode);
header(HttpConstants.Header.LOCATION, location);
header(HttpConstants.Header.CONNECTION, "close");
try {
httpServletResponse.sendError(statusCode);
} catch (IOException e) {
throw new PippoRuntimeException(e);
}
}
/**
* Set the response status to OK (200).
*
* Standard response for successful HTTP requests. The actual response will
* depend on the request method used. In a GET request, the response will
* contain an entity corresponding to the requested resource. In a POST
* request the response will contain an entity describing or containing the
* result of the action.
*
*
*/
public Response ok() {
status(HttpConstants.StatusCode.OK);
return this;
}
/**
* Set the response status to CREATED (201).
*
* The request has been fulfilled and resulted in a new resource being created.
*
*
*/
public Response created() {
status(HttpConstants.StatusCode.CREATED);
return this;
}
/**
* Set the response status to ACCEPTED (202).
*
* The request has been accepted for processing, but the processing has not
* been completed. The request might or might not eventually be acted upon,
* as it might be disallowed when processing actually takes place.
*
*
*/
public Response accepted() {
status(HttpConstants.StatusCode.ACCEPTED);
return this;
}
/**
* Set the response status to BAD REQUEST (400).
*
* The server cannot or will not process the request due to something that
* is perceived to be a client error.
*
*
*/
public Response badRequest() {
status(HttpConstants.StatusCode.BAD_REQUEST);
return this;
}
/**
* Set the response status to UNAUTHORIZED (401).
*
* Similar to 403 Forbidden, but specifically for use when authentication is
* required and has failed or has not yet been provided. The response must
* include a WWW-Authenticate header field containing a challenge applicable
* to the requested resource.
*
*/
public Response unauthorized() {
status(HttpConstants.StatusCode.UNAUTHORIZED);
return this;
}
/**
* Set the response status to PAYMENT REQUIRED (402).
*
* Reserved for future use. The original intention was that this code might
* be used as part of some form of digital cash or micropayment scheme, but
* that has not happened, and this code is not usually used.
*
*/
public Response paymentRequired() {
status(HttpConstants.StatusCode.PAYMENT_REQUIRED);
return this;
}
/**
* Set the response status to FORBIDDEN (403).
*
* The request was a valid request, but the server is refusing to respond to
* it. Unlike a 401 Unauthorized response, authenticating will make no
* difference.
*
*
*/
public Response forbidden() {
status(HttpConstants.StatusCode.FORBIDDEN);
return this;
}
/**
* Set the response status to NOT FOUND (404).
*
* The requested resource could not be found but may be available again in
* the future. Subsequent requests by the client are permissible.
*
*
*/
public Response notFound() {
status(HttpConstants.StatusCode.NOT_FOUND);
return this;
}
/**
* Set the response status to METHOD NOT ALLOWED (405).
*
* A request was made of a resource using a request method not supported
* by that resource; for example, using GET on a form which requires data
* to be presented via POST, or using PUT on a read-only resource.
*
*
*/
public Response methodNotAllowed() {
status(HttpConstants.StatusCode.METHOD_NOT_ALLOWED);
return this;
}
/**
* Set the response status to CONFLICT (409).
*
* Indicates that the request could not be processed because of conflict in
* the request, such as an edit conflict in the case of multiple updates.
*
*
*/
public Response conflict() {
status(HttpConstants.StatusCode.CONFLICT);
return this;
}
/**
* Set the response status to GONE (410).
*
* Indicates that the resource requested is no longer available and will not
* be available again. This should be used when a resource has been
* intentionally removed and the resource should be purged. Upon receiving a
* 410 status code, the client should not request the resource again in the
* future.
*
*
*/
public Response gone() {
status(HttpConstants.StatusCode.GONE);
return this;
}
/**
* Set the response status to INTERNAL ERROR (500).
*
* A generic error message, given when an unexpected condition was
* encountered and no more specific message is suitable.
*
*
*/
public Response internalError() {
status(HttpConstants.StatusCode.INTERNAL_ERROR);
return this;
}
/**
* Set the response status to NOT IMPLEMENTED (501).
*
* The server either does not recognize the request method, or it lacks the
* ability to fulfil the request. Usually this implies future availability
* (e.g., a new feature of a web-service API).
*
*
*/
public Response notImplemented() {
status(HttpConstants.StatusCode.NOT_IMPLEMENTED);
return this;
}
/**
* Set the response status to OVERLOADED (502).
*
* The server is currently unavailable (because it is overloaded or down
* for maintenance). Generally, this is a temporary state.
*
*/
public Response overloaded() {
status(HttpConstants.StatusCode.OVERLOADED);
return this;
}
/**
* Set the response status to SERVICE UNAVAILABLE (503).
*
* The server is currently unavailable (because it is overloaded or down
* for maintenance). Generally, this is a temporary state.
*
*/
public Response serviceUnavailable() {
status(HttpConstants.StatusCode.SERVICE_UNAVAILABLE);
return this;
}
/**
* Sets the content length of the response.
*
* @param length
* @return the response
*/
public Response contentLength(long length) {
checkCommitted();
httpServletResponse.setContentLength((int) length);
return this;
}
/**
* Returns the content type of the response.
*
* @return the content type
*/
public String getContentType() {
return httpServletResponse.getContentType();
}
/**
* Sets the content type of the response.
*
* @param contentType
* @return the response
*/
public Response contentType(String contentType) {
checkCommitted();
httpServletResponse.setContentType(contentType);
return this;
}
/**
* Attempts to set the Content-Type of the Response based on Request
* headers.
*
* The Accept header is preferred for negotiation but the Content-Type
* header may also be used if an agreeable engine can not be determined.
*
*
* If no Content-Type can not be negotiated then the response will not be
* modified. This behavior allows specification of a default Content-Type
* using one of the methods such as xml()
or json()
.
*
* For example, response.xml().contentType(request).send(myObject);
* would set the default Content-Type as application/xml
and
* then attempt to negotiate the client's preferred type. If negotiation failed,
* then the default application/xml
would be sent and used to
* serialize the outgoing object.
*
* @param request
* @return the response
*/
public Response contentType(Request request) {
// prefer the Accept header
if ("*/*".equals(request.getAcceptType())) {
// client accepts all types
return this;
}
ContentTypeEngine engine = contentTypeEngines.getContentTypeEngine(request.getAcceptType());
if (engine != null) {
log.debug("Negotiated '{}' from request Accept header", engine.getContentType());
} else if (!StringUtils.isNullOrEmpty(request.getContentType())) {
// try to match the Request content-type
engine = contentTypeEngines.getContentTypeEngine(request.getContentType());
if (engine != null) {
log.debug("Negotiated '{}' from request Content-Type header", engine.getContentType());
}
}
if (engine == null) {
log.debug("Failed to negotiate a content type for Accept='{}' and Content-Type='{}'",
request.getAcceptType(), request.getContentType());
return this;
}
return contentType(engine.getContentType());
}
/**
* Sets the Response content-type to text/plain.
*/
public Response text() {
return contentType(HttpConstants.ContentType.TEXT_PLAIN);
}
/**
* Sets the Response content-type to text/html.
*/
public Response html() {
return contentType(HttpConstants.ContentType.TEXT_HTML);
}
/**
* Sets the Response content-type to application/json.
*/
public Response json() {
return contentType(HttpConstants.ContentType.APPLICATION_JSON);
}
/**
* Sets the Response content-type to application/xml.
*/
public Response xml() {
return contentType(HttpConstants.ContentType.APPLICATION_XML);
}
/**
* Sets the Response content-type to application/x-yaml.
*/
public Response yaml() {
return contentType(HttpConstants.ContentType.APPLICATION_X_YAML);
}
/**
* Writes the string content directly to the response.
*
This method commits the response.
*
* @param content
*/
public void send(CharSequence content) {
checkCommitted();
commit(content);
}
/**
* Replaces '{}' in the format string with the supplied arguments and
* writes the string content directly to the response.
* This method commits the response.
*
* @param format
* @param args
*/
public void send(String format, Object... args) {
checkCommitted();
commit(StringUtils.format(format, args));
}
/**
* Serializes the object as JSON using the registered application/json
* ContentTypeEngine and writes it to the response.
* This method commits the response.
*
* @param object
*/
public void json(Object object) {
send(object, HttpConstants.ContentType.APPLICATION_JSON);
}
/**
* Serializes the object as XML using the registered application/xml
* ContentTypeEngine and writes it to the response.
* This method commits the response.
*
* @param object
*/
public void xml(Object object) {
send(object, HttpConstants.ContentType.APPLICATION_XML);
}
/**
* Serializes the object as YAML using the registered application/x-yaml
* ContentTypeEngine and writes it to the response.
* This method commits the response.
*
* @param object
*/
public void yaml(Object object) {
send(object, HttpConstants.ContentType.APPLICATION_X_YAML);
}
/**
* Serializes the object as plain text using the registered text/plain
* ContentTypeEngine and writes it to the response.
* This method commits the response.
*
* @param object
*/
public void text(Object object) {
send(object, HttpConstants.ContentType.TEXT_PLAIN);
}
/**
* Serializes the object using the registered ContentTypeEngine matching
* the pre-specified content-type.
* This method commits the response.
*
* @param object
*/
public void send(Object object) {
send(object, getContentType());
}
private void send(Object object, String contentType) {
if (StringUtils.isNullOrEmpty(contentType)) {
throw new PippoRuntimeException("You must specify a content type!");
}
ContentTypeEngine contentTypeEngine = contentTypeEngines.getContentTypeEngine(contentType);
if (contentTypeEngine == null) {
throw new PippoRuntimeException("You must set a content type engine for '{}'", contentType);
}
header(HttpConstants.Header.CONTENT_TYPE, contentTypeEngine.getContentType());
send(contentTypeEngine.toString(object));
}
/**
* Copies the input stream to the response output stream and closes the input stream upon completion.
* This method commits the response.
*
* @param input
*/
public void resource(InputStream input) {
checkCommitted();
finalizeResponse();
// content type to OCTET_STREAM if it's not set
if (getContentType() == null) {
contentType(HttpConstants.ContentType.APPLICATION_OCTET_STREAM);
}
try {
// by calling httpServletResponse.getOutputStream() we are committing the response
IoUtils.copy(input, httpServletResponse.getOutputStream());
// flushing the buffer forces chunked-encoding
httpServletResponse.flushBuffer();
} catch (Exception e) {
throw new PippoRuntimeException(e);
} finally {
IoUtils.close(input);
}
}
/**
* Writes the specified file directly to the response.
* This method commits the response.
*
* @param file
*/
public void file(File file) {
try {
file(file.getName(), new FileInputStream(file));
} catch (FileNotFoundException e) {
throw new PippoRuntimeException(e);
}
}
/**
* Copies the input stream to the response output stream as a download and closes the input stream upon completion.
* This method commits the response.
*
* @param filename
* @param input
*/
public void file(String filename, InputStream input) {
checkCommitted();
// content type to OCTET_STREAM if it's not set
if (getContentType() == null) {
contentType(mimeTypes.getContentType(filename, HttpConstants.ContentType.APPLICATION_OCTET_STREAM));
}
if (isHeaderEmpty(HttpConstants.Header.CONTENT_DISPOSITION)) {
if (filename != null && !filename.isEmpty()) {
header(HttpConstants.Header.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"");
} else {
header(HttpConstants.Header.CONTENT_DISPOSITION, "attachment; filename=\"\"");
}
}
finalizeResponse();
try {
// by calling httpServletResponse.getOutputStream() we are committing the response
IoUtils.copy(input, httpServletResponse.getOutputStream());
// flushing the buffer forces chunked-encoding
httpServletResponse.flushBuffer();
} catch (Exception e) {
throw new PippoRuntimeException(e);
} finally {
IoUtils.close(input);
}
}
/**
* Renders a template and writes the output directly to the response.
* This method commits the response.
*
* @param templateName
*/
public void render(String templateName) {
render(templateName, new HashMap());
}
/**
* Renders a template and writes the output directly to the response.
* This method commits the response.
*
* @param templateName
* @param model
*/
public void render(String templateName, Map model) {
if (templateEngine == null) {
log.error("You must set a template engine first");
return;
}
// merge the model passed with the locals data
model.putAll(getLocals());
// add session (if exists) to model
Session session = Session.get();
if (session != null) {
// model.put("session", session.getAll());
model.put("session", session);
}
// render the template using the merged model
StringWriter stringWriter = new StringWriter();
templateEngine.renderResource(templateName, model, stringWriter);
send(stringWriter.toString());
}
private void checkCommitted() {
if (isCommitted()) {
throw new PippoRuntimeException("The response has already been committed");
}
}
/**
* Returns true if this response has already been committed.
*
* @return true if the response has been committed
*/
public boolean isCommitted() {
return httpServletResponse.isCommitted();
}
/**
* This method commits the response.
*/
public void commit() {
commit(null);
}
private void commit(CharSequence content) {
checkCommitted();
finalizeResponse();
// content type to TEXT_HTML if it's not set
if (getContentType() == null) {
contentType(HttpConstants.ContentType.TEXT_HTML);
}
try {
if (content != null) {
httpServletResponse.getWriter().append(content);
}
log.trace("Response committed");
httpServletResponse.flushBuffer();
} catch (IOException e) {
throw new PippoRuntimeException(e);
}
}
private void finalizeResponse() {
// add headers
for (Map.Entry header : getHeaderMap().entrySet()) {
httpServletResponse.setHeader(header.getKey(), header.getValue());
}
// add cookies
for (Cookie cookie : getCookies()) {
httpServletResponse.addCookie(cookie);
}
// set status to OK if it's not set
if (getStatus() == 0 || getStatus() == Integer.MAX_VALUE) {
ok();
}
// call finalize listeners
if ((finalizeListeners != null) && !finalizeListeners.isEmpty()) {
finalizeListeners.onFinalize(this);
}
}
public ResponseFinalizeListenerList getFinalizeListeners() {
if (finalizeListeners == null) {
finalizeListeners = new ResponseFinalizeListenerList();
}
return finalizeListeners;
}
public OutputStream getOutputStream() {
checkCommitted();
finalizeResponse();
// content type to OCTET_STREAM if it's not set
if (getContentType() == null) {
contentType(HttpConstants.ContentType.APPLICATION_OCTET_STREAM);
}
try {
return httpServletResponse.getOutputStream();
} catch (IOException e) {
throw new PippoRuntimeException(e);
}
}
public static Response get() {
RouteContext routeContext = RouteDispatcher.getRouteContext();
return (routeContext != null) ? routeContext.getResponse() : null;
}
}