/*
* Request.java
*
* This work 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 2 of the License,
* or (at your option) any later version.
*
* This work 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, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*
* Copyright (c) 2004-2006 Per Cederberg. All rights reserved.
*/
package org.liquidsite.core.web;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.liquidsite.core.content.User;
import org.liquidsite.util.log.Log;
/**
* An HTTP request and response.
*
* @author Per Cederberg, <per at percederberg dot net>
* @version 1.0
*/
public class Request {
/**
* The class logger.
*/
private static final Log LOG = new Log(Request.class);
/**
* The no response type. This type is used when no request
* response has been issued.
*/
private static final int NO_RESPONSE = 0;
/**
* The data response type. This type is used when a data string
* has been set as the request response.
*/
private static final int DATA_RESPONSE = 1;
/**
* The file response type. This type is used when a file has been
* set as the request response. The response data contains the
* absolute file name when this type is set.
*/
private static final int FILE_RESPONSE = 2;
/**
* The redirect response type. This type is used when a request
* redirect has been issued. The response data contains the
* redirect URI (absolute or relative) when this type is set.
*/
private static final int REDIRECT_RESPONSE = 4;
/**
* The error response type. This type is used when a response
* error code should be sent. The response code, MIME type and
* data string may be set when sending this response.
*/
private static final int ERROR_RESPONSE = 5;
/**
* The HTTP servlet context.
*/
private ServletContext context;
/**
* The HTTP request.
*/
private HttpServletRequest request;
/**
* The HTTP response.
*/
private HttpServletResponse response;
/**
* The reqponse type. This flag is set to true if the
* response object has been modified.
*/
private int responseType = NO_RESPONSE;
/**
* The response HTTP code. Only used when sending error responses.
*/
private int responseCode = HttpServletResponse.SC_OK;
/**
* The response MIME type.
*/
private String responseMimeType = null;
/**
* The response data.
*/
private String responseData = null;
/**
* The response limited cache flag.
*/
private boolean responseLimitCache = false;
/**
* The request environment.
*/
private RequestEnvironment environment = new RequestEnvironment();
/**
* The request session.
*/
private RequestSession session = null;
/**
* Creates a new request.
*
* @param context the servlet context
* @param request the HTTP request
* @param response the HTTP response
*/
public Request(ServletContext context,
HttpServletRequest request,
HttpServletResponse response) {
this.context = context;
this.request = request;
this.response = response;
if (request.getCharacterEncoding() == null) {
try {
request.setCharacterEncoding("UTF-8");
} catch (UnsupportedEncodingException ignore) {
// Do nothing
}
}
}
/**
* Returns a string representation of this request.
*
* @return a string representation of this request
*/
public String toString() {
return getUrl();
}
/**
* Checks if this request was sent in a session. If a new session
* was created as a result of processing, this method will still
* return false.
*
* @return true if the request has an associated session, or
* false otherwise
*/
public boolean hasSession() {
return request.getSession(false) != null
&& !request.getSession().isNew();
}
/**
* Checks if this request contains a response.
*
* @return true if the request contains a response, or
* false otherwise
*/
public boolean hasResponse() {
return responseType != NO_RESPONSE;
}
/**
* Returns the protocol name in the request, i.e. "http" or
* "https".
*
* @return the protocol name
*/
public String getProtocol() {
return request.getScheme();
}
/**
* Returns the host name in the request.
*
* @return the host name
*/
public String getHost() {
return request.getServerName();
}
/**
* Returns the port number in the request.
*
* @return the port number
*/
public int getPort() {
return request.getServerPort();
}
/**
* Returns the request path with file name. This will include the
* servlet portion of the path.
*
* @return the request path with file name
*/
public String getPath() {
String path = request.getPathInfo();
if (path == null) {
return request.getContextPath();
} else {
return request.getContextPath() + path;
}
}
/**
* Returns the servlet context.
*
* @return the servlet context
*/
public ServletContext getServletContext() {
return context;
}
/**
* Returns the servlet portion of the request path.
*
* @return the servlet portion of the request path
*/
public String getServletPath() {
return request.getContextPath();
}
/**
* Returns the full request URL with protocol, hostname and path.
* No query parameters will be included in the URL, however.
*
* @return the full request URL
*/
public String getUrl() {
return request.getRequestURL().toString();
}
/**
* Returns the IP address of the request sender.
*
* @return the IP address of the request sender
*/
public String getRemoteAddr() {
return request.getRemoteAddr();
}
/**
* Returns the value of a request attribute.
*
* @param name the attribute name
*
* @return the attribute value, or
* null if no such attribute was found
*/
public Object getAttribute(String name) {
return getAttribute(name, null);
}
/**
* Returns the value of a request attribute. If the specified
* attribute does not exist, a default value will be returned.
*
* @param name the attribute name
* @param defVal the default attribute value
*
* @return the attribute value, or
* the default value if no such attribute was found
*/
public Object getAttribute(String name, Object defVal) {
Object value = request.getAttribute(name);
return (value == null) ? defVal : value;
}
/**
* Returns all the request attributes in a map.
*
* @return the map with request attribute names and values
*/
public Map getAllAttributes() {
HashMap map = new HashMap();
Enumeration names;
String name;
names = request.getAttributeNames();
while (names.hasMoreElements()) {
name = names.nextElement().toString();
map.put(name, request.getAttribute(name));
}
return map;
}
/**
* Sets a request attribute value.
*
* @param name the attribute name
* @param value the attribute value
*/
public void setAttribute(String name, Object value) {
request.setAttribute(name, value);
}
/**
* Sets a request attribute value.
*
* @param name the attribute name
* @param value the attribute value
*/
public void setAttribute(String name, boolean value) {
setAttribute(name, new Boolean(value));
}
/**
* Sets a request attribute value.
*
* @param name the attribute name
* @param value the attribute value
*/
public void setAttribute(String name, int value) {
setAttribute(name, new Integer(value));
}
/**
* Returns the value of a request header.
*
* @param name the request header name
*
* @return the request header value, or
* null if no such header was found
*/
public String getHeader(String name) {
return request.getHeader(name);
}
/**
* Returns a map with all the request parameter names and values.
*
* @return the map with request parameter names and values
*/
public Map getAllParameters() {
HashMap params = new HashMap();
Enumeration names = request.getParameterNames();
String name;
while (names.hasMoreElements()) {
name = names.nextElement().toString();
params.put(name, request.getParameter(name));
}
return params;
}
/**
* Returns the value of a request parameter.
*
* @param name the request parameter name
*
* @return the request parameter value, or
* null if no such parameter was found
*/
public String getParameter(String name) {
return getParameter(name, null);
}
/**
* Returns the value of a request parameter. If the specified
* parameter does not exits, a default value will be returned.
*
* @param name the request parameter name
* @param defVal the default parameter value
*
* @return the request parameter value, or
* the default value if no such parameter was found
*/
public String getParameter(String name, String defVal) {
String value = request.getParameter(name);
return (value == null) ? defVal : value;
}
/**
* Returns the specified file request parameter. The default
* request container doesn't support file parameters, and will
* return null on each call. Other request containers may
* override this method, however, if they are able to handle
* incoming files.
*
* @param name the request parameter name
*
* @return the request file parameter, or
* null if no such file parameter was found
*/
public FileParameter getFileParameter(String name) {
return null;
}
/**
* Returns the session user. The session user is null until it is
* set by the setUser() method. Normally the session user is not
* set until the user has been authenticated. If no sesssion
* existed, this method will return null.
*
* @return the session user, or
* null if no user has been set
*
* @see #setUser
*/
public User getUser() {
if (request.getSession(false) == null) {
return null;
} else {
return getSession().getUser();
}
}
/**
* Sets the session user. The session user is null until it is
* set by this method. Normally the session user is not set until
* the user has been authenticated. If the user is set to null,
* the whole session will be invalidated.
*
* @param user the new session user, or null to logout
*
* @see #getUser
*/
public void setUser(User user) {
if (user == null) {
if (request.getSession(false) != null) {
getSession().invalidate();
session = null;
}
} else {
getSession().setUser(user);
}
}
/**
* Returns the request environment object. The request
* environment is used for storing references to objects that the
* request touches.
*
* @return the request environment object
*/
public RequestEnvironment getEnvironment() {
return environment;
}
/**
* Returns the request session. If no session existed a new one
* will be created.
*
* @return the new or existing request session
*/
public RequestSession getSession() {
if (session == null) {
session = new RequestSession(request.getSession());
}
return session;
}
/**
* Clears any previously sent but non-committed response.
*/
public void sendClear() {
responseType = NO_RESPONSE;
responseCode = HttpServletResponse.SC_OK;
responseMimeType = null;
responseData = null;
responseLimitCache = false;
}
/**
* Sends the specified data as the request response.
*
* @param mimeType the data MIME type
* @param data the data to send
*/
public void sendData(String mimeType, String data) {
sendClear();
responseType = DATA_RESPONSE;
responseMimeType = mimeType;
responseData = data;
}
/**
* Sends the contents of a file as the request response. The file
* name extension will be used for determining the MIME type for
* the file contents. The cache header for the file may be
* limited to private by setting the limit cache header.
*
* @param file the file containing the response
* @param limitCache the limited cache flag
*/
public void sendFile(File file, boolean limitCache) {
sendClear();
responseType = FILE_RESPONSE;
responseMimeType = null;
responseData = file.toString();
responseLimitCache = limitCache;
}
/**
* Redirects this request by sending a temporary redirection URL
* to the browser. The location specified may be either an
* absolute or a relative URL. This method will set a request
* response.
*
* @param location the destination location
*/
public void sendRedirect(String location) {
sendClear();
responseType = REDIRECT_RESPONSE;
responseData = location;
}
/**
* Sends the specified error code and data as the request response.
*
* @param code the HTTP response code to send
* @param mimeType the data MIME type
* @param data the data to send
*/
public void sendError(int code, String mimeType, String data) {
sendClear();
responseType = ERROR_RESPONSE;
responseCode = code;
responseMimeType = mimeType;
responseData = data;
}
/**
* Disposes of all resources used by this request object. This
* method shouldn't be called until a response has been written.
*/
public void dispose() {
sendClear();
request = null;
response = null;
environment = null;
session = null;
}
/**
* Sends the request response to the underlying HTTP response
* object. This method shouldn't be called more than once per
* request, and should not be called in case no response has been
* stored in the request. The response can be committed either
* completely or solely with the response headers.
*
* @param context the servlet context
* @param content the complete content response flag
*
* @throws IOException if an IO error occured while attempting to
* commit the response
* @throws ServletException if a configuration error was
* encountered while sending the response
*/
public void commit(ServletContext context, boolean content)
throws IOException, ServletException {
switch (responseType) {
case DATA_RESPONSE:
commitData(content);
break;
case FILE_RESPONSE:
commitFile(context, content);
break;
case REDIRECT_RESPONSE:
commitRedirect();
break;
case ERROR_RESPONSE:
commitError(content);
break;
default:
throw new ServletException("No request response available: " +
this);
}
}
/**
* Sets the dynamic HTTP response headers. The current system time
* will be used as the last modification time.
*/
private void commitDynamicHeaders() {
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "-1");
response.setDateHeader("Last-Modified", System.currentTimeMillis());
}
/**
* Sets the static HTTP response headers. The specified system
* time will be used as the last modification time.
*
* @param lastModified the last modification time, or
* zero (0) for the current system time
*/
private void commitStaticHeaders(long lastModified) {
if (responseLimitCache) {
response.setHeader("Cache-Control", "private");
} else {
response.setHeader("Cache-Control", "public");
}
if (lastModified > 0) {
response.setDateHeader("Last-Modified", lastModified);
} else {
response.setDateHeader("Last-Modified",
System.currentTimeMillis());
}
}
/**
* Sends the data response to the underlying HTTP response object.
* The response can be committed either completely or solely with
* the response headers.
*
* @param content the complete content response flag
*
* @throws IOException if an IO error occured while attempting to
* commit the response
*/
private void commitData(boolean content) throws IOException {
PrintWriter out;
LOG.info("Handling request for " + this + " with string data");
commitDynamicHeaders();
if (responseMimeType.indexOf("charset") > 0) {
response.setContentType(responseMimeType);
} else {
response.setContentType(responseMimeType + "; charset=UTF-8");
}
if (content) {
out = response.getWriter();
out.write(responseData);
out.close();
}
}
/**
* Sends the file response to the underlying HTTP response object.
* The response can be committed either completely or solely with
* the response headers.
*
* @param context the servlet context
* @param content the complete content response flag
*
* @throws IOException if an IO error occured while attempting to
* commit the response
*/
private void commitFile(ServletContext context, boolean content)
throws IOException {
File file;
long modified;
FileInputStream input;
OutputStream output;
byte[] buffer = new byte[4096];
int length;
LOG.info("Handling request for " + this + " with file " +
responseData);
file = new File(responseData);
modified = request.getDateHeader("If-Modified-Since");
if (modified != -1 && file.lastModified() < modified + 1000) {
LOG.trace("request response: HTTP 304, file " + file +
" not modified");
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
commitStaticHeaders(file.lastModified());
response.setContentType(context.getMimeType(responseData));
response.setContentLength((int) file.length());
if (content) {
try {
input = new FileInputStream(file);
} catch (IOException e) {
LOG.error("failed to read HTTP response file " + file, e);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
output = response.getOutputStream();
while ((length = input.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
input.close();
output.close();
}
}
/**
* Sends the redirect response to the underlying HTTP response
* object.
*
* @throws IOException if an IO error occured while attempting to
* redirect the request
*/
private void commitRedirect() throws IOException {
LOG.info("Redirecting request for " + this + " to " + responseData);
commitDynamicHeaders();
response.sendRedirect(responseData);
}
/**
* Sends the error response to the underlying HTTP response object.
* The response can be committed either completely or solely with
* the response headers.
*
* @param content the complete content response flag
*
* @throws IOException if an IO error occured while attempting to
* commit the response
*/
private void commitError(boolean content) throws IOException {
PrintWriter out;
LOG.info("Handling request for " + this + " with error code");
response.setStatus(responseCode);
commitDynamicHeaders();
if (responseMimeType.indexOf("charset") > 0) {
response.setContentType(responseMimeType);
} else {
response.setContentType(responseMimeType + "; charset=UTF-8");
}
if (content) {
out = response.getWriter();
out.write(responseData);
out.close();
}
}
/**
* A request file parameter.
*
* @author Per Cederberg, <per at percederberg dot net>
* @version 1.0
*/
public interface FileParameter {
/**
* Returns the base file name including the extension. The
* file name returned is guaranteed to not contain any file
* path or directory name.
*
* @return the base file name (with extension)
*/
String getName();
/**
* Returns the full file name including path and extension.
* The file name returned should be exactly the one sent by
* the browser.
*
* @return the full file path
*/
String getPath();
/**
* Returns the file size.
*
* @return the file size
*/
long getSize();
/**
* Writes this file to a temporary file in the upload
* directory. After calling this method, no other methods in
* this interfaced should be called.
*
* @return the file created
*
* @throws IOException if the file parameter couldn't be
* written
*/
File write() throws IOException;
/**
* Writes this file to the specified destination file. After
* calling this method, no other methods in this interface
* should be called.
*
* @param dest the destination file
*
* @throws IOException if the file parameter couldn't be
* written to the specified file
*/
void write(File dest) throws IOException;
}
}